feat: add integration with redux devtools extension

Currently supports:
- Tracking actions and navigation state
- Time travel for navigation state

It doesn't do anything in production
This commit is contained in:
satyajit.happy
2019-08-17 11:42:13 +05:30
committed by Satyajit Sahoo
parent b7735af7fc
commit ca985bb96a
5 changed files with 141 additions and 4 deletions

View File

@@ -25,6 +25,9 @@ const NavigationBuilderContext = React.createContext<{
addActionListener?: (listener: ChildActionListener) => void;
addFocusedListener?: (listener: FocusedNavigationListener) => void;
onRouteFocus?: (key: string) => void;
}>({});
trackAction: (key: string, action: NavigationAction) => void;
}>({
trackAction: () => undefined,
});
export default NavigationBuilderContext;

View File

@@ -3,6 +3,7 @@ import * as BaseActions from './BaseActions';
import EnsureSingleNavigator from './EnsureSingleNavigator';
import NavigationBuilderContext from './NavigationBuilderContext';
import useFocusedListeners from './useFocusedListeners';
import useDevTools from './useDevTools';
import {
Route,
@@ -133,12 +134,25 @@ const Container = React.forwardRef(function NavigationContainer(
const transactionStateRef = React.useRef<State | null>(null);
const isTransactionActiveRef = React.useRef<boolean>(false);
const isFirstMountRef = React.useRef<boolean>(true);
const skipTrackingRef = React.useRef<boolean>(false);
const reset = React.useCallback((state: NavigationState) => {
skipTrackingRef.current = true;
setNavigationState(state);
}, []);
const { trackState, trackAction } = useDevTools({
name: '@navigation-ex',
reset,
state,
});
const builderContext = React.useMemo(
() => ({
addFocusedListener,
trackAction,
}),
[addFocusedListener]
[addFocusedListener, trackAction]
);
const performTransaction = React.useCallback((callback: () => void) => {
@@ -189,6 +203,12 @@ const Container = React.forwardRef(function NavigationContainer(
);
React.useEffect(() => {
if (skipTrackingRef.current) {
skipTrackingRef.current = false;
} else {
trackState(state);
}
navigationStateRef.current = state;
transactionStateRef.current = null;
@@ -197,7 +217,7 @@ const Container = React.forwardRef(function NavigationContainer(
}
isFirstMountRef.current = false;
}, [state, onStateChange]);
}, [state, onStateChange, trackState]);
return (
<NavigationBuilderContext.Provider value={builderContext}>

View File

@@ -61,6 +61,7 @@ export default function useDescriptors<
emitter,
}: Options<ScreenOptions>) {
const [options, setOptions] = React.useState<{ [key: string]: object }>({});
const { trackAction } = React.useContext(NavigationBuilderContext);
const context = React.useMemo(
() => ({
@@ -69,8 +70,16 @@ export default function useDescriptors<
addActionListener,
addFocusedListener,
onRouteFocus,
trackAction,
}),
[navigation, onAction, addActionListener, addFocusedListener, onRouteFocus]
[
navigation,
onAction,
addActionListener,
addFocusedListener,
onRouteFocus,
trackAction,
]
);
const navigations = useNavigationCache<State, ScreenOptions>({

View File

@@ -0,0 +1,101 @@
import * as React from 'react';
import { NavigationState, NavigationAction, PartialState } from './types';
type State = NavigationState | PartialState<NavigationState> | undefined;
type Options = {
name: string;
reset: (state: NavigationState) => void;
state: State;
};
type DevTools = {
init(value: any): void;
send(action: any, value: any): void;
subscribe(
listener: (message: { type: string; [key: string]: any }) => void
): () => void;
};
declare global {
namespace NodeJS {
interface Global {
__REDUX_DEVTOOLS_EXTENSION__:
| {
connect(options: { name: string }): DevTools;
disconnect(): void;
}
| undefined;
}
}
}
export default function useDevTools({ name, reset, state }: Options) {
const devToolsRef = React.useRef<DevTools>();
if (
process.env.NODE_ENV !== 'production' &&
global.__REDUX_DEVTOOLS_EXTENSION__ &&
devToolsRef.current === undefined
) {
devToolsRef.current = global.__REDUX_DEVTOOLS_EXTENSION__.connect({ name });
}
const devTools = devToolsRef.current;
const lastStateRef = React.useRef<State>(state);
const actions = React.useRef<
Array<{ key: string; action: NavigationAction }>
>([]);
React.useEffect(() => {
devTools && devTools.init(lastStateRef.current);
}, [devTools]);
React.useEffect(
() =>
devTools &&
devTools.subscribe(message => {
if (message.type === 'DISPATCH' && message.state) {
reset(JSON.parse(message.state));
}
}),
[devTools, reset]
);
const trackState = React.useCallback(
(state: State) => {
if (!devTools) {
return;
}
while (actions.current.length > 1) {
devTools.send(actions.current.shift(), lastStateRef.current);
}
if (actions.current.length) {
devTools.send(actions.current.pop(), state);
} else if (lastStateRef.current !== state) {
devTools.send('@@UNKNOWN', state);
}
lastStateRef.current = state;
},
[devTools]
);
const trackAction = React.useCallback(
(key: string, action: NavigationAction) => {
if (!devTools) {
return;
}
actions.current.push({ key, action });
},
[devTools]
);
return {
trackAction,
trackState,
};
}

View File

@@ -32,6 +32,7 @@ export default function useOnAction({
onAction: onActionParent,
onRouteFocus: onRouteFocusParent,
addActionListener: addActionListenerParent,
trackAction,
} = React.useContext(NavigationBuilderContext);
const onAction = React.useCallback(
@@ -60,6 +61,8 @@ export default function useOnAction({
result = result === null && action.target === state.key ? state : result;
if (result !== null) {
trackAction(state.key, action);
if (state !== result) {
setState(result);
}
@@ -99,6 +102,7 @@ export default function useOnAction({
getState,
router,
onActionParent,
trackAction,
onRouteFocusParent,
setState,
key,