mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-04-29 04:45:19 +08:00
refactor: add a type property to router and state (#146)
The `type` property denotes the type of the router. It can be used to verify compatibility of navigation state and the router when rehydrating state, making rehydration more resilient. It can also help our utilities to detect the type of the navigator to properly implement some functionality. For example, the `useScrollToTop` hook can now know if it's not inside a tab navigator and needs to find the tab navigator in a parent.
This commit is contained in:
committed by
Michał Osadnik
parent
fb749ac064
commit
3d9db6ff25
@@ -4,7 +4,8 @@ import * as CommonActions from '../CommonActions';
|
||||
jest.mock('shortid', () => () => 'test');
|
||||
|
||||
const STATE = {
|
||||
stale: false as false,
|
||||
stale: false as const,
|
||||
type: 'test',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routes: [
|
||||
@@ -23,6 +24,7 @@ it('replaces focused screen with REPLACE', () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
stale: false,
|
||||
type: 'test',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routes: [
|
||||
@@ -42,6 +44,7 @@ it('replaces source screen with REPLACE', () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
stale: false,
|
||||
type: 'test',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routes: [
|
||||
@@ -70,6 +73,7 @@ it('sets params for the focused screen with SET_PARAMS', () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
stale: false,
|
||||
type: 'test',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routes: [
|
||||
@@ -89,6 +93,7 @@ it('sets params for the source screen with SET_PARAMS', () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
stale: false,
|
||||
type: 'test',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routes: [
|
||||
|
||||
@@ -233,6 +233,7 @@ it('handle dispatching with ref', () => {
|
||||
expect(onStateChange).toBeCalledTimes(1);
|
||||
expect(onStateChange).lastCalledWith({
|
||||
stale: false,
|
||||
type: 'test',
|
||||
index: 0,
|
||||
key: '0',
|
||||
routeNames: ['foo', 'foo2', 'bar', 'baz'],
|
||||
@@ -242,6 +243,7 @@ it('handle dispatching with ref', () => {
|
||||
name: 'baz',
|
||||
state: {
|
||||
stale: false,
|
||||
type: 'test',
|
||||
index: 0,
|
||||
key: '1',
|
||||
routeNames: ['qux', 'lex'],
|
||||
@@ -367,10 +369,12 @@ it('handle getRootState', () => {
|
||||
routeNames: ['qux', 'lex'],
|
||||
routes: [{ key: 'qux', name: 'qux' }, { key: 'lex', name: 'lex' }],
|
||||
stale: false,
|
||||
type: 'test',
|
||||
},
|
||||
},
|
||||
{ key: 'bar', name: 'bar' },
|
||||
],
|
||||
stale: false,
|
||||
type: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,8 @@ export const MockRouterKey = { current: 0 };
|
||||
|
||||
export default function MockRouter(options: DefaultRouterOptions) {
|
||||
const router: Router<NavigationState, MockActions> = {
|
||||
type: 'test',
|
||||
|
||||
getInitialState({ routeNames, routeParamList }) {
|
||||
const index =
|
||||
options.initialRouteName === undefined
|
||||
@@ -21,6 +23,7 @@ export default function MockRouter(options: DefaultRouterOptions) {
|
||||
|
||||
return {
|
||||
stale: false,
|
||||
type: 'test',
|
||||
key: String(MockRouterKey.current++),
|
||||
index,
|
||||
routeNames,
|
||||
@@ -58,6 +61,7 @@ export default function MockRouter(options: DefaultRouterOptions) {
|
||||
|
||||
return {
|
||||
stale: false,
|
||||
type: 'test',
|
||||
key: String(MockRouterKey.current++),
|
||||
index:
|
||||
typeof state.index === 'number' && state.index < routes.length
|
||||
|
||||
@@ -52,6 +52,7 @@ it('initializes state for a navigator on navigation', () => {
|
||||
expect(onStateChange).toBeCalledTimes(1);
|
||||
expect(onStateChange).toBeCalledWith({
|
||||
stale: false,
|
||||
type: 'test',
|
||||
index: 0,
|
||||
key: '0',
|
||||
routeNames: ['foo', 'bar', 'baz'],
|
||||
@@ -106,6 +107,66 @@ it('rehydrates state for a navigator on navigation', () => {
|
||||
routeNames: ['foo', 'bar'],
|
||||
routes: [{ key: 'foo', name: 'foo' }, { key: 'bar', name: 'bar' }],
|
||||
stale: false,
|
||||
type: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't rehydrate state if the type of state didn't match router", () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return descriptors[state.routes[state.index].key].render();
|
||||
};
|
||||
|
||||
const FooScreen = (props: any) => {
|
||||
React.useEffect(() => {
|
||||
props.navigation.dispatch({ type: 'UPDATE' });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
index: 1,
|
||||
type: 'something-else',
|
||||
routes: [{ key: 'foo', name: 'foo' }, { key: 'bar', name: 'bar' }],
|
||||
};
|
||||
|
||||
const onStateChange = jest.fn();
|
||||
|
||||
const element = (
|
||||
<NavigationContainer
|
||||
initialState={initialState}
|
||||
onStateChange={onStateChange}
|
||||
>
|
||||
<TestNavigator initialRouteName="foo">
|
||||
<Screen
|
||||
name="foo"
|
||||
component={FooScreen}
|
||||
initialParams={{ answer: 42 }}
|
||||
/>
|
||||
<Screen name="bar" component={jest.fn()} />
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
render(element).update(element);
|
||||
|
||||
expect(onStateChange).lastCalledWith({
|
||||
index: 0,
|
||||
key: '0',
|
||||
routeNames: ['foo', 'bar'],
|
||||
routes: [
|
||||
{
|
||||
key: 'foo',
|
||||
name: 'foo',
|
||||
params: { answer: 42 },
|
||||
},
|
||||
{ key: 'bar', name: 'bar' },
|
||||
],
|
||||
stale: false,
|
||||
type: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -144,6 +205,7 @@ it('initializes state for nested screens in React.Fragment', () => {
|
||||
expect(onStateChange).toBeCalledTimes(1);
|
||||
expect(onStateChange).toBeCalledWith({
|
||||
stale: false,
|
||||
type: 'test',
|
||||
index: 0,
|
||||
key: '0',
|
||||
routeNames: ['foo', 'bar', 'baz'],
|
||||
@@ -194,6 +256,7 @@ it('initializes state for nested navigator on navigation', () => {
|
||||
expect(onStateChange).toBeCalledTimes(1);
|
||||
expect(onStateChange).toBeCalledWith({
|
||||
stale: false,
|
||||
type: 'test',
|
||||
index: 2,
|
||||
key: '0',
|
||||
routeNames: ['foo', 'bar', 'baz'],
|
||||
@@ -205,6 +268,7 @@ it('initializes state for nested navigator on navigation', () => {
|
||||
name: 'baz',
|
||||
state: {
|
||||
stale: false,
|
||||
type: 'test',
|
||||
index: 0,
|
||||
key: '1',
|
||||
routeNames: ['qux'],
|
||||
@@ -309,6 +373,7 @@ it('cleans up state when the navigator unmounts', () => {
|
||||
expect(onStateChange).toBeCalledTimes(1);
|
||||
expect(onStateChange).lastCalledWith({
|
||||
stale: false,
|
||||
type: 'test',
|
||||
index: 0,
|
||||
key: '0',
|
||||
routeNames: ['foo', 'bar'],
|
||||
@@ -361,6 +426,7 @@ it('allows state updates by dispatching a function returning an action', () => {
|
||||
expect(onStateChange).toBeCalledTimes(1);
|
||||
expect(onStateChange).toBeCalledWith({
|
||||
stale: false,
|
||||
type: 'test',
|
||||
index: 1,
|
||||
key: '0',
|
||||
routeNames: ['foo', 'bar'],
|
||||
@@ -399,6 +465,7 @@ it('updates route params with setParams', () => {
|
||||
expect(onStateChange).toBeCalledTimes(1);
|
||||
expect(onStateChange).lastCalledWith({
|
||||
stale: false,
|
||||
type: 'test',
|
||||
index: 0,
|
||||
key: '0',
|
||||
routeNames: ['foo', 'bar'],
|
||||
@@ -413,6 +480,7 @@ it('updates route params with setParams', () => {
|
||||
expect(onStateChange).toBeCalledTimes(2);
|
||||
expect(onStateChange).lastCalledWith({
|
||||
stale: false,
|
||||
type: 'test',
|
||||
index: 0,
|
||||
key: '0',
|
||||
routeNames: ['foo', 'bar'],
|
||||
@@ -470,6 +538,7 @@ it('updates route params with setParams applied to parent', () => {
|
||||
{ key: 'bar', name: 'bar' },
|
||||
],
|
||||
stale: false,
|
||||
type: 'test',
|
||||
});
|
||||
|
||||
act(() => setParams({ age: 25 }));
|
||||
@@ -484,6 +553,7 @@ it('updates route params with setParams applied to parent', () => {
|
||||
{ key: 'bar', name: 'bar' },
|
||||
],
|
||||
stale: false,
|
||||
type: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -516,6 +586,7 @@ it('handles change in route names', () => {
|
||||
|
||||
expect(onStateChange).toBeCalledWith({
|
||||
stale: false,
|
||||
type: 'test',
|
||||
index: 0,
|
||||
key: '0',
|
||||
routeNames: ['foo', 'baz', 'qux'],
|
||||
@@ -628,6 +699,7 @@ it('gives access to internal state', () => {
|
||||
routeNames: ['bar'],
|
||||
routes: [{ key: 'bar', name: 'bar' }],
|
||||
stale: false,
|
||||
type: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -216,6 +216,7 @@ it('fires blur event when a route is removed with a delay', async () => {
|
||||
|
||||
return {
|
||||
stale: false,
|
||||
type: 'test',
|
||||
key: 'stack',
|
||||
index: 0,
|
||||
routeNames,
|
||||
|
||||
@@ -76,6 +76,7 @@ it("lets parent handle the action if child didn't", () => {
|
||||
expect(onStateChange).toBeCalledTimes(1);
|
||||
expect(onStateChange).lastCalledWith({
|
||||
stale: false,
|
||||
type: 'test',
|
||||
index: 2,
|
||||
key: '0',
|
||||
routeNames: ['foo', 'bar', 'baz'],
|
||||
@@ -191,6 +192,7 @@ it("lets children handle the action if parent didn't", () => {
|
||||
expect(onStateChange).toBeCalledTimes(1);
|
||||
expect(onStateChange).lastCalledWith({
|
||||
stale: false,
|
||||
type: 'test',
|
||||
index: 0,
|
||||
key: '0',
|
||||
routeNames: ['foo', 'bar', 'baz'],
|
||||
@@ -200,6 +202,7 @@ it("lets children handle the action if parent didn't", () => {
|
||||
name: 'baz',
|
||||
state: {
|
||||
stale: false,
|
||||
type: 'test',
|
||||
index: 0,
|
||||
key: '1',
|
||||
routeNames: ['qux', 'lex'],
|
||||
|
||||
@@ -23,6 +23,12 @@ export type NavigationState = {
|
||||
routes: Array<
|
||||
Route<string> & { state?: NavigationState | PartialState<NavigationState> }
|
||||
>;
|
||||
/**
|
||||
* Custom type for the state, whether it's for tab, stack, drawer etc.
|
||||
* During rehydration, the state will be discarded if type doesn't match with router type.
|
||||
* It can also be used to detect the type of the navigator we're dealing with.
|
||||
*/
|
||||
type: string;
|
||||
/**
|
||||
* Whether the navigation state has been rehydrated.
|
||||
*/
|
||||
@@ -36,9 +42,10 @@ export type InitialState = Partial<
|
||||
};
|
||||
|
||||
export type PartialState<State extends NavigationState> = Partial<
|
||||
Omit<State, 'stale' | 'key' | 'routes' | 'routeNames'>
|
||||
Omit<State, 'stale' | 'type' | 'key' | 'routes' | 'routeNames'>
|
||||
> & {
|
||||
stale?: true;
|
||||
type?: string;
|
||||
routes: Array<
|
||||
Omit<Route<string>, 'key'> & { key?: string; state?: InitialState }
|
||||
>;
|
||||
@@ -115,6 +122,12 @@ export type Router<
|
||||
State extends NavigationState,
|
||||
Action extends NavigationAction
|
||||
> = {
|
||||
/**
|
||||
* Type of the router. Should match the `type` property in state.
|
||||
* If the type doesn't match, the state will be discarded during rehydration.
|
||||
*/
|
||||
type: State['type'];
|
||||
|
||||
/**
|
||||
* Initialize the navigation state.
|
||||
*
|
||||
|
||||
@@ -166,6 +166,17 @@ export default function useNavigationBuilder<
|
||||
);
|
||||
}
|
||||
|
||||
const isStateValid = React.useCallback(
|
||||
state => state.type === undefined || state.type === router.type,
|
||||
[router.type]
|
||||
);
|
||||
|
||||
const isStateInitialized = React.useCallback(
|
||||
state =>
|
||||
state !== undefined && state.stale === false && isStateValid(state),
|
||||
[isStateValid]
|
||||
);
|
||||
|
||||
const {
|
||||
state: currentState,
|
||||
getState: getCurrentState,
|
||||
@@ -188,7 +199,7 @@ export default function useNavigationBuilder<
|
||||
// Otherwise assume that the state was provided as initial state
|
||||
// So we need to rehydrate it to make it usable
|
||||
initializedStateRef.current =
|
||||
currentState === undefined
|
||||
currentState === undefined || !isStateValid(currentState)
|
||||
? router.getInitialState({
|
||||
routeNames,
|
||||
routeParamList,
|
||||
@@ -207,9 +218,9 @@ export default function useNavigationBuilder<
|
||||
// If the state isn't initialized, or stale, use the state we initialized instead
|
||||
// The state won't update until there's a change needed in the state we have initalized locally
|
||||
// So it'll be `undefined` or stale untill the first navigation event happens
|
||||
currentState === undefined || currentState.stale !== false
|
||||
? (initializedStateRef.current as State)
|
||||
: (currentState as State);
|
||||
isStateInitialized(currentState)
|
||||
? (currentState as State)
|
||||
: (initializedStateRef.current as State);
|
||||
|
||||
let nextState: State = state;
|
||||
|
||||
@@ -271,10 +282,10 @@ export default function useNavigationBuilder<
|
||||
const getState = React.useCallback((): State => {
|
||||
const currentState = getCurrentState();
|
||||
|
||||
return currentState === undefined || currentState.stale !== false
|
||||
? (initializedStateRef.current as State)
|
||||
: (currentState as State);
|
||||
}, [getCurrentState]);
|
||||
return isStateInitialized(currentState)
|
||||
? (currentState as State)
|
||||
: (initializedStateRef.current as State);
|
||||
}, [getCurrentState, isStateInitialized]);
|
||||
|
||||
const emitter = useEventEmitter();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user