mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-04-28 12:25:21 +08:00
feat: initial implementation of a flipper plugin
This commit is contained in:
@@ -36,20 +36,23 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/core": "^6.0.0-next.8",
|
||||
"deep-equal": "^2.0.5"
|
||||
"deep-equal": "^2.0.5",
|
||||
"nanoid": "^3.1.22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-navigation/core": "^6.0.0-next.8",
|
||||
"@testing-library/react-native": "^7.2.0",
|
||||
"@types/deep-equal": "^1.0.1",
|
||||
"@types/react": "^16.9.53",
|
||||
"del-cli": "^3.0.1",
|
||||
"react": "~16.13.1",
|
||||
"react-native-builder-bob": "^0.18.1",
|
||||
"react-native-flipper": "^0.80.0",
|
||||
"typescript": "^4.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
"react": "*",
|
||||
"react-native-flipper": "*"
|
||||
},
|
||||
"react-native-builder-bob": {
|
||||
"source": "src",
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
const noop: any = () => {};
|
||||
|
||||
export let useReduxDevToolsExtension: typeof import('./useReduxDevToolsExtension').default;
|
||||
export let useFlipper: typeof import('./useFlipper').default;
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
useReduxDevToolsExtension = require('./useReduxDevToolsExtension').default;
|
||||
useFlipper = require('./useFlipper').default;
|
||||
} else {
|
||||
useReduxDevToolsExtension = noop;
|
||||
useFlipper = noop;
|
||||
}
|
||||
|
||||
111
packages/devtools/src/useDevToolsBase.tsx
Normal file
111
packages/devtools/src/useDevToolsBase.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import * as React from 'react';
|
||||
import type {
|
||||
NavigationContainerRef,
|
||||
NavigationState,
|
||||
NavigationAction,
|
||||
} from '@react-navigation/core';
|
||||
import deepEqual from 'deep-equal';
|
||||
|
||||
export default function useDevToolsBase(
|
||||
ref: React.RefObject<NavigationContainerRef<any>>,
|
||||
callback: (
|
||||
...args:
|
||||
| [type: 'init', state: NavigationState | undefined]
|
||||
| [
|
||||
type: 'action',
|
||||
action: NavigationAction,
|
||||
state: NavigationState | undefined
|
||||
]
|
||||
) => void
|
||||
) {
|
||||
const lastStateRef = React.useRef<NavigationState | undefined>();
|
||||
const lastActionRef = React.useRef<NavigationAction | undefined>();
|
||||
const callbackRef = React.useRef(callback);
|
||||
const lastResetRef = React.useRef<NavigationState | undefined>(undefined);
|
||||
|
||||
React.useEffect(() => {
|
||||
callbackRef.current = callback;
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
let timer: number;
|
||||
let unsubscribeAction: (() => void) | undefined;
|
||||
let unsubscribeState: (() => void) | undefined;
|
||||
|
||||
const initialize = async () => {
|
||||
if (!ref.current) {
|
||||
// If the navigation object isn't ready yet, wait for it
|
||||
await new Promise<void>((resolve) => {
|
||||
timer = setInterval(() => {
|
||||
if (ref.current) {
|
||||
resolve();
|
||||
clearTimeout(timer);
|
||||
const state = ref.current.getRootState();
|
||||
|
||||
lastStateRef.current = state;
|
||||
callbackRef.current('init', state);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
const navigation = ref.current!;
|
||||
|
||||
unsubscribeAction = navigation.addListener('__unsafe_action__', (e) => {
|
||||
const action = e.data.action;
|
||||
|
||||
if (e.data.noop) {
|
||||
// Even if the state didn't change, it's useful to show the action
|
||||
callbackRef.current('action', action, lastStateRef.current);
|
||||
} else {
|
||||
lastActionRef.current = action;
|
||||
}
|
||||
});
|
||||
|
||||
unsubscribeState = navigation.addListener('state', (e) => {
|
||||
// Don't show the action in dev tools if the state is what we sent to reset earlier
|
||||
if (
|
||||
lastResetRef.current &&
|
||||
deepEqual(lastResetRef.current, e.data.state)
|
||||
) {
|
||||
lastStateRef.current = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const state = navigation.getRootState();
|
||||
const lastState = lastStateRef.current;
|
||||
const action = lastActionRef.current;
|
||||
|
||||
lastActionRef.current = undefined;
|
||||
lastStateRef.current = state;
|
||||
|
||||
// If we don't have an action and the state didn't change, then it's probably extraneous
|
||||
if (action === undefined && deepEqual(state, lastState)) {
|
||||
return;
|
||||
}
|
||||
|
||||
callbackRef.current('action', action ?? { type: '@@UNKNOWN' }, state);
|
||||
});
|
||||
};
|
||||
|
||||
initialize();
|
||||
|
||||
return () => {
|
||||
unsubscribeAction?.();
|
||||
unsubscribeState?.();
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [ref]);
|
||||
|
||||
const resetRoot = React.useCallback(
|
||||
(state: NavigationState) => {
|
||||
if (ref.current) {
|
||||
lastResetRef.current = state;
|
||||
ref.current.resetRoot(state);
|
||||
}
|
||||
},
|
||||
[ref]
|
||||
);
|
||||
|
||||
return { resetRoot };
|
||||
}
|
||||
95
packages/devtools/src/useFlipper.tsx
Normal file
95
packages/devtools/src/useFlipper.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import * as React from 'react';
|
||||
import { addPlugin, Flipper } from 'react-native-flipper';
|
||||
import type { NavigationContainerRef } from '@react-navigation/core';
|
||||
import { nanoid } from 'nanoid/non-secure';
|
||||
import useDevToolsBase from './useDevToolsBase';
|
||||
|
||||
export default function useFlipper(
|
||||
ref: React.RefObject<NavigationContainerRef<any>>
|
||||
) {
|
||||
const connectionRef = React.useRef<Flipper.FlipperConnection>();
|
||||
|
||||
const { resetRoot } = useDevToolsBase(ref, (...args) => {
|
||||
const connection = connectionRef.current;
|
||||
|
||||
if (!connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (args[0]) {
|
||||
case 'init':
|
||||
connection.send('init', {
|
||||
id: nanoid(),
|
||||
state: args[1],
|
||||
});
|
||||
break;
|
||||
case 'action':
|
||||
connection.send('action', {
|
||||
id: nanoid(),
|
||||
action: args[1],
|
||||
state: args[2],
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
addPlugin({
|
||||
getId() {
|
||||
return 'react-navigation';
|
||||
},
|
||||
async onConnect(connection) {
|
||||
connectionRef.current = connection;
|
||||
|
||||
const on = (event: string, listener: (params: any) => Promise<any>) => {
|
||||
connection.receive(event, (params, responder) => {
|
||||
try {
|
||||
const result = listener(params);
|
||||
|
||||
// Return null instead of undefined, otherwise flipper doesn't respond
|
||||
responder.success(result ?? null);
|
||||
} catch (e) {
|
||||
responder.error(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
on('navigation.invoke', ({ method, args = [] }) => {
|
||||
switch (method) {
|
||||
case 'resetRoot':
|
||||
return resetRoot(args[0]);
|
||||
default:
|
||||
// @ts-expect-error: we want to call arbitrary methods here
|
||||
return ref.current?.[method](...args);
|
||||
}
|
||||
});
|
||||
|
||||
on('linking.invoke', ({ method, args = [] }) => {
|
||||
// @ts-expect-error: __linking isn't publicly exposed
|
||||
const linking = ref.current?.__linking;
|
||||
|
||||
switch (method) {
|
||||
case 'getStateFromPath':
|
||||
case 'getPathFromState':
|
||||
case 'getActionFromState':
|
||||
return linking?.[method](
|
||||
args[0],
|
||||
args[1]?.trim()
|
||||
? // eslint-disable-next-line no-eval
|
||||
eval(`(function() { return ${args[1]}; }())`)
|
||||
: linking.config
|
||||
);
|
||||
default:
|
||||
return linking?.[method](...args);
|
||||
}
|
||||
});
|
||||
},
|
||||
onDisconnect() {
|
||||
connectionRef.current = undefined;
|
||||
},
|
||||
runInBackground() {
|
||||
return false;
|
||||
},
|
||||
});
|
||||
}, [ref, resetRoot]);
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import type {
|
||||
NavigationContainerRef,
|
||||
NavigationState,
|
||||
NavigationAction,
|
||||
} from '@react-navigation/core';
|
||||
import deepEqual from 'deep-equal';
|
||||
import type { NavigationContainerRef } from '@react-navigation/core';
|
||||
import useDevToolsBase from './useDevToolsBase';
|
||||
|
||||
type DevToolsConnection = {
|
||||
init(value: any): void;
|
||||
@@ -35,8 +31,22 @@ export default function useReduxDevToolsExtension(
|
||||
});
|
||||
}
|
||||
|
||||
const lastStateRef = React.useRef<NavigationState | undefined>();
|
||||
const lastActionRef = React.useRef<NavigationAction | undefined>();
|
||||
const { resetRoot } = useDevToolsBase(ref, (...args) => {
|
||||
const devTools = devToolsRef.current;
|
||||
|
||||
if (!devTools) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (args[0]) {
|
||||
case 'init':
|
||||
devTools.init(args[1]);
|
||||
break;
|
||||
case 'action':
|
||||
devTools.send(args[1], args[2]);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
React.useEffect(
|
||||
() =>
|
||||
@@ -44,66 +54,9 @@ export default function useReduxDevToolsExtension(
|
||||
if (message.type === 'DISPATCH' && message.state) {
|
||||
const state = JSON.parse(message.state);
|
||||
|
||||
lastStateRef.current = state;
|
||||
ref.current?.resetRoot(state);
|
||||
resetRoot(state);
|
||||
}
|
||||
}),
|
||||
[ref]
|
||||
[resetRoot]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const devTools = devToolsRef.current;
|
||||
const navigation = ref.current;
|
||||
|
||||
if (!navigation || !devTools) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastStateRef.current === undefined) {
|
||||
const state = navigation.getRootState();
|
||||
|
||||
devTools.init(state);
|
||||
lastStateRef.current = state;
|
||||
}
|
||||
|
||||
const unsubscribeAction = navigation.addListener(
|
||||
'__unsafe_action__',
|
||||
(e) => {
|
||||
const action = e.data.action;
|
||||
|
||||
if (e.data.noop) {
|
||||
// Even if the state didn't change, it's useful to show the action
|
||||
devTools.send(action, lastStateRef.current);
|
||||
} else {
|
||||
lastActionRef.current = action;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const unsubscribeState = navigation.addListener('state', (e) => {
|
||||
// Don't show the action in dev tools if the state is what we sent to reset earlier
|
||||
if (lastStateRef.current === e.data.state) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastState = lastStateRef.current;
|
||||
const state = navigation.getRootState();
|
||||
const action = lastActionRef.current;
|
||||
|
||||
lastActionRef.current = undefined;
|
||||
lastStateRef.current = state;
|
||||
|
||||
// If we don't have an action and the state didn't change, then it's probably extraneous
|
||||
if (action === undefined && deepEqual(state, lastState)) {
|
||||
return;
|
||||
}
|
||||
|
||||
devTools.send(action ?? '@@UNKNOWN', state);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribeAction();
|
||||
unsubscribeState();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user