fix: make sure new state events are emitted when new navigators mount

This commit is contained in:
Satyajit Sahoo
2020-07-19 14:52:43 +02:00
parent b2a99c2a88
commit af8b27414c
6 changed files with 138 additions and 10 deletions

View File

@@ -247,6 +247,10 @@ const BaseNavigationContainer = React.forwardRef(
[scheduleUpdate, flushUpdates]
);
const isInitialRef = React.useRef(true);
const getIsInitial = React.useCallback(() => isInitialRef.current, []);
const context = React.useMemo(
() => ({
state,
@@ -254,14 +258,24 @@ const BaseNavigationContainer = React.forwardRef(
setState,
getKey,
setKey,
getIsInitial,
addOptionsGetter,
}),
[getKey, getState, setKey, setState, state, addOptionsGetter]
[
state,
getState,
setState,
getKey,
setKey,
getIsInitial,
addOptionsGetter,
]
);
const onStateChangeRef = React.useRef(onStateChange);
React.useEffect(() => {
isInitialRef.current = false;
onStateChangeRef.current = onStateChange;
});

View File

@@ -13,6 +13,7 @@ export default React.createContext<{
setState: (
state: NavigationState | PartialState<NavigationState> | undefined
) => void;
getIsInitial: () => boolean;
addOptionsGetter?: (
key: string,
getter: () => object | undefined | null
@@ -32,4 +33,7 @@ export default React.createContext<{
get setState(): any {
throw new Error(MISSING_CONTEXT_ERROR);
},
get getIsInitial(): any {
throw new Error(MISSING_CONTEXT_ERROR);
},
});

View File

@@ -76,6 +76,14 @@ export default function SceneView<
[getState, route.key, setState]
);
const isInitialRef = React.useRef(true);
React.useEffect(() => {
isInitialRef.current = false;
});
const getIsInitial = React.useCallback(() => isInitialRef.current, []);
const context = React.useMemo(
() => ({
state: route.state,
@@ -83,14 +91,16 @@ export default function SceneView<
setState: setCurrentState,
getKey,
setKey,
getIsInitial,
addOptionsGetter,
}),
[
getCurrentState,
getKey,
route.state,
getCurrentState,
setCurrentState,
getKey,
setKey,
getIsInitial,
addOptionsGetter,
]
);

View File

@@ -366,6 +366,7 @@ it('handles getRootState', () => {
type: 'test',
});
});
it('emits state events when the state changes', () => {
const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
@@ -401,6 +402,7 @@ it('emits state events when the state changes', () => {
ref.current?.navigate('bar');
});
expect(listener).toBeCalledTimes(1);
expect(listener.mock.calls[0][0].data.state).toEqual({
type: 'test',
stale: false,
@@ -418,6 +420,7 @@ it('emits state events when the state changes', () => {
ref.current?.navigate('baz', { answer: 42 });
});
expect(listener).toBeCalledTimes(2);
expect(listener.mock.calls[1][0].data.state).toEqual({
type: 'test',
stale: false,
@@ -432,6 +435,97 @@ it('emits state events when the state changes', () => {
});
});
it('emits state events when new navigator mounts', () => {
jest.useFakeTimers();
const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return (
<React.Fragment>
{state.routes.map((route) => descriptors[route.key].render())}
</React.Fragment>
);
};
const ref = React.createRef<NavigationContainerRef>();
const NestedNavigator = () => {
const [isRendered, setIsRendered] = React.useState(false);
React.useEffect(() => {
setTimeout(() => setIsRendered(true), 100);
}, []);
if (!isRendered) {
return null;
}
return (
<TestNavigator>
<Screen name="baz">{() => null}</Screen>
<Screen name="bax">{() => null}</Screen>
</TestNavigator>
);
};
const onStateChange = jest.fn();
const element = (
<BaseNavigationContainer ref={ref} onStateChange={onStateChange}>
<TestNavigator>
<Screen name="foo">{() => null}</Screen>
<Screen name="bar" component={NestedNavigator} />
</TestNavigator>
</BaseNavigationContainer>
);
render(element).update(element);
const listener = jest.fn();
ref.current?.addListener('state', listener);
expect(listener).not.toHaveBeenCalled();
expect(onStateChange).not.toHaveBeenCalled();
act(() => {
jest.runAllTimers();
});
const resultState = {
stale: false,
type: 'test',
index: 0,
key: '10',
routeNames: ['foo', 'bar'],
routes: [
{ key: 'foo', name: 'foo' },
{
key: 'bar',
name: 'bar',
state: {
stale: false,
type: 'test',
index: 0,
key: '11',
routeNames: ['baz', 'bax'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bax', name: 'bax' },
],
},
},
],
};
expect(listener).toBeCalledTimes(1);
expect(listener.mock.calls[0][0].data.state).toEqual(resultState);
expect(onStateChange).toBeCalledTimes(1);
expect(onStateChange).lastCalledWith(resultState);
});
it('emits option events when options change with tab router', () => {
const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(TabRouter, props);

View File

@@ -279,6 +279,7 @@ export default function useNavigationBuilder<
setState,
setKey,
getKey,
getIsInitial,
} = React.useContext(NavigationStateContext);
const [initializedState, isFirstStateInitialization] = React.useMemo(() => {
@@ -372,6 +373,13 @@ export default function useNavigationBuilder<
React.useEffect(() => {
setKey(navigatorKey);
if (!getIsInitial()) {
// If it's not initial render, we need to update the state
// This will make sure that our container gets notifier of state changes due to new mounts
// This is necessary for proper screen tracking, URL updates etc.
setState(nextState);
}
return () => {
// We need to clean up state for this navigator on unmount
// We do it in a timeout because we need to detect if another navigator mounted in the meantime