mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-04-29 04:45:19 +08:00
fix: make sure new state events are emitted when new navigators mount
This commit is contained in:
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user