fix: automatically queue listeners when container isn't ready

This commit is contained in:
Satyajit Sahoo
2021-08-25 03:05:18 +02:00
parent 8b65aea2db
commit acdde18d89
3 changed files with 112 additions and 15 deletions

View File

@@ -192,12 +192,13 @@ const BaseNavigationContainer = React.forwardRef(
return acc;
}, {}),
...emitter.create('root'),
resetRoot,
dispatch,
resetRoot,
isFocused: () => true,
canGoBack,
getRootState,
getState: () => stateRef.current,
getParent: () => undefined,
getState: () => stateRef.current,
getRootState,
getCurrentRoute,
getCurrentOptions,
isReady: () => listeners.focus[0] != null,

View File

@@ -0,0 +1,47 @@
import type { NavigationState, ParamListBase } from '@react-navigation/routers';
import { render } from '@testing-library/react-native';
import * as React from 'react';
import BaseNavigationContainer from '../BaseNavigationContainer';
import createNavigationContainerRef from '../createNavigationContainerRef';
import Screen from '../Screen';
import useNavigationBuilder from '../useNavigationBuilder';
import MockRouter from './__fixtures__/MockRouter';
it('adds the listener even if container is mounted later', () => {
const ref = createNavigationContainerRef<ParamListBase>();
const listener = jest.fn();
ref.addListener('state', listener);
const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder<
NavigationState,
any,
{},
{ title?: string },
any
>(MockRouter, props);
const { render, options } = descriptors[state.routes[state.index].key];
return (
<main>
<h1>{options.title}</h1>
<div>{render()}</div>
</main>
);
};
const element = (
<BaseNavigationContainer ref={ref}>
<TestNavigator>
<Screen name="foo">{() => null}</Screen>
<Screen name="bar">{() => null}</Screen>
</TestNavigator>
</BaseNavigationContainer>
);
render(element).update(element);
expect(listener).toHaveBeenCalledTimes(1);
});

View File

@@ -1,6 +1,10 @@
import { CommonActions } from '@react-navigation/routers';
import type { NavigationContainerRefWithCurrent } from './types';
import type {
NavigationContainerEventMap,
NavigationContainerRef,
NavigationContainerRefWithCurrent,
} from './types';
export const NOT_INITIALIZED_ERROR =
"The 'navigation' object hasn't been initialized yet. This might happen if you don't have a navigator mounted, or if the navigator hasn't finished mounting. See https://reactnavigation.org/docs/navigating-without-navigation-prop#handling-initialization for more details.";
@@ -14,6 +18,7 @@ export default function createNavigationContainerRef<
'removeListener',
'resetRoot',
'dispatch',
'isFocused',
'canGoBack',
'getRootState',
'getState',
@@ -22,26 +27,70 @@ export default function createNavigationContainerRef<
'getCurrentOptions',
] as const;
const listeners: Record<string, ((...args: any[]) => void)[]> = {};
const removeListener = (
event: string,
callback: (...args: any[]) => void
) => {
listeners[event] = listeners[event]?.filter((cb) => cb !== callback);
};
let current: NavigationContainerRef<ParamList> | null = null;
const ref: NavigationContainerRefWithCurrent<ParamList> = {
get current() {
return current;
},
set current(value: NavigationContainerRef<ParamList> | null) {
current = value;
if (value != null) {
Object.entries(listeners).forEach(([event, callbacks]) => {
callbacks.forEach((callback) => {
value.addListener(
event as keyof NavigationContainerEventMap,
callback
);
});
});
}
},
isReady: () => {
if (current == null) {
return false;
}
return current.isReady();
},
...methods.reduce<any>((acc, name) => {
acc[name] = (...args: any[]) => {
if (ref.current == null) {
console.error(NOT_INITIALIZED_ERROR);
if (current == null) {
switch (name) {
case 'addListener': {
const [event, callback] = args;
listeners[event] = listeners[event] || [];
listeners[event].push(callback);
return () => removeListener(event, callback);
}
case 'removeListener': {
const [event, callback] = args;
removeListener(event, callback);
break;
}
default:
console.error(NOT_INITIALIZED_ERROR);
}
} else {
// @ts-expect-error: this is ok
return ref.current[name](...args);
return current[name](...args);
}
};
return acc;
}, {}),
isReady: () => {
if (ref.current == null) {
return false;
}
return ref.current.isReady();
},
current: null,
};
return ref;