Skip to content

Commit fe3f98e

Browse files
authored
feat: add event for options on container (facebook#8334)
1 parent 35d6b9e commit fe3f98e

File tree

8 files changed

+144
-45
lines changed

8 files changed

+144
-45
lines changed

packages/core/src/BaseNavigationContainer.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,13 +210,24 @@ const BaseNavigationContainer = React.forwardRef(
210210
[emitter]
211211
);
212212

213+
const onOptionsChange = React.useCallback(
214+
(options) => {
215+
emitter.emit({
216+
type: 'options',
217+
data: { options },
218+
});
219+
},
220+
[emitter]
221+
);
222+
213223
const builderContext = React.useMemo(
214224
() => ({
215225
addFocusedListener,
216226
addStateGetter,
217227
onDispatchAction,
228+
onOptionsChange,
218229
}),
219-
[addFocusedListener, addStateGetter, onDispatchAction]
230+
[addFocusedListener, addStateGetter, onDispatchAction, onOptionsChange]
220231
);
221232

222233
const scheduleContext = React.useMemo(

packages/core/src/NavigationBuilderContext.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ const NavigationBuilderContext = React.createContext<{
3434
onRouteFocus?: (key: string) => void;
3535
onDispatchAction: (action: NavigationAction, noop: boolean) => void;
3636
addStateGetter?: (key: string, getter: NavigatorStateGetter) => void;
37+
onOptionsChange: (options: object) => void;
3738
}>({
3839
onDispatchAction: () => undefined,
40+
onOptionsChange: () => undefined,
3941
});
4042

4143
export default NavigationBuilderContext;

packages/core/src/SceneView.tsx

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import {
66
PartialState,
77
} from '@react-navigation/routers';
88
import NavigationStateContext from './NavigationStateContext';
9-
import NavigationContext from './NavigationContext';
10-
import NavigationRouteContext from './NavigationRouteContext';
119
import StaticContainer from './StaticContainer';
1210
import EnsureSingleNavigator from './EnsureSingleNavigator';
13-
import { NavigationProp, RouteConfig, EventMapBase } from './types';
1411
import useOptionsGetters from './useOptionsGetters';
12+
import NavigationBuilderContext from './NavigationBuilderContext';
13+
import useFocusEffect from './useFocusEffect';
14+
import { NavigationProp, RouteConfig, EventMapBase } from './types';
1515

1616
type Props<
1717
State extends NavigationState,
@@ -45,21 +45,25 @@ export default function SceneView<
4545
options,
4646
}: Props<State, ScreenOptions, EventMap>) {
4747
const navigatorKeyRef = React.useRef<string | undefined>();
48+
const { onOptionsChange } = React.useContext(NavigationBuilderContext);
4849
const getKey = React.useCallback(() => navigatorKeyRef.current, []);
49-
5050
const optionsRef = React.useRef<object | undefined>(options);
51-
52-
React.useEffect(() => {
53-
optionsRef.current = options;
54-
}, [options]);
55-
5651
const getOptions = React.useCallback(() => optionsRef.current, []);
5752

58-
const { addOptionsGetter } = useOptionsGetters({
53+
const { addOptionsGetter, hasAnyChildListener } = useOptionsGetters({
5954
key: route.key,
6055
getOptions,
6156
});
6257

58+
const optionsChange = React.useCallback(() => {
59+
optionsRef.current = options;
60+
if (!hasAnyChildListener) {
61+
onOptionsChange(options);
62+
}
63+
}, [onOptionsChange, options, hasAnyChildListener]);
64+
65+
useFocusEffect(optionsChange);
66+
6367
const setKey = React.useCallback((key: string) => {
6468
navigatorKeyRef.current = key;
6569
}, []);
@@ -105,28 +109,24 @@ export default function SceneView<
105109
);
106110

107111
return (
108-
<NavigationContext.Provider value={navigation}>
109-
<NavigationRouteContext.Provider value={route}>
110-
<NavigationStateContext.Provider value={context}>
111-
<EnsureSingleNavigator>
112-
<StaticContainer
113-
name={screen.name}
114-
// @ts-ignore
115-
render={screen.component || screen.children}
116-
navigation={navigation}
117-
route={route}
118-
>
119-
{'component' in screen && screen.component !== undefined ? (
120-
// @ts-ignore
121-
<screen.component navigation={navigation} route={route} />
122-
) : 'children' in screen && screen.children !== undefined ? (
123-
// @ts-ignore
124-
screen.children({ navigation, route })
125-
) : null}
126-
</StaticContainer>
127-
</EnsureSingleNavigator>
128-
</NavigationStateContext.Provider>
129-
</NavigationRouteContext.Provider>
130-
</NavigationContext.Provider>
112+
<NavigationStateContext.Provider value={context}>
113+
<EnsureSingleNavigator>
114+
<StaticContainer
115+
name={screen.name}
116+
// @ts-ignore
117+
render={screen.component || screen.children}
118+
navigation={navigation}
119+
route={route}
120+
>
121+
{'component' in screen && screen.component !== undefined ? (
122+
// @ts-ignore
123+
<screen.component navigation={navigation} route={route} />
124+
) : 'children' in screen && screen.children !== undefined ? (
125+
// @ts-ignore
126+
screen.children({ navigation, route })
127+
) : null}
128+
</StaticContainer>
129+
</EnsureSingleNavigator>
130+
</NavigationStateContext.Provider>
131131
);
132132
}

packages/core/src/__tests__/BaseNavigationContainer.test.tsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,65 @@ it('emits state events when the state changes', () => {
430430
});
431431
});
432432

433+
it('emits state events when options change', () => {
434+
const TestNavigator = (props: any) => {
435+
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
436+
437+
return (
438+
<React.Fragment>
439+
{state.routes.map((route) => descriptors[route.key].render())}
440+
</React.Fragment>
441+
);
442+
};
443+
444+
const ref = React.createRef<NavigationContainerRef>();
445+
446+
const element = (
447+
<BaseNavigationContainer ref={ref}>
448+
<TestNavigator>
449+
<Screen name="foo" options={{ x: 1 }}>
450+
{() => null}
451+
</Screen>
452+
<Screen name="bar" options={{ y: 2 }}>
453+
{() => null}
454+
</Screen>
455+
<Screen name="baz" options={{ v: 3 }}>
456+
{() => (
457+
<TestNavigator>
458+
<Screen name="foo" options={{ g: 5 }}>
459+
{() => null}
460+
</Screen>
461+
</TestNavigator>
462+
)}
463+
</Screen>
464+
</TestNavigator>
465+
</BaseNavigationContainer>
466+
);
467+
468+
const listener = jest.fn();
469+
470+
render(element).update(element);
471+
ref.current?.addListener('options', listener);
472+
473+
act(() => {
474+
ref.current?.navigate('bar');
475+
});
476+
477+
expect(listener.mock.calls[0][0].data.options).toEqual({
478+
y: 2,
479+
});
480+
481+
ref.current?.removeListener('options', listener);
482+
const listener2 = jest.fn();
483+
ref.current?.addListener('options', listener2);
484+
485+
act(() => {
486+
ref.current?.navigate('baz');
487+
});
488+
489+
expect(listener2.mock.calls[0][0].data.options).toEqual({ g: 5 });
490+
});
491+
433492
it('throws if there is no navigator rendered', () => {
434493
expect.assertions(1);
435494

packages/core/src/types.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export type EventMapCore<State extends NavigationState> = {
3737
focus: { data: undefined };
3838
blur: { data: undefined };
3939
state: { data: { state: State } };
40+
options: { data: { options: object } };
4041
};
4142

4243
export type EventArg<
@@ -422,6 +423,10 @@ export type NavigationContainerEventMap = {
422423
state: NavigationState;
423424
};
424425
};
426+
/**
427+
* Event which fires when current options changes.
428+
*/
429+
options: { data: { options: object } };
425430
/**
426431
* Event which fires when an action is dispatched.
427432
* Only intended for debugging purposes, don't use it for app logic.

packages/core/src/useDescriptors.tsx

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import {
2020
RouteProp,
2121
EventMapBase,
2222
} from './types';
23+
import NavigationContext from './NavigationContext';
24+
import NavigationRouteContext from './NavigationRouteContext';
2325

2426
type Options<
2527
State extends NavigationState,
@@ -80,7 +82,9 @@ export default function useDescriptors<
8082
emitter,
8183
}: Options<State, ScreenOptions, EventMap>) {
8284
const [options, setOptions] = React.useState<Record<string, object>>({});
83-
const { onDispatchAction } = React.useContext(NavigationBuilderContext);
85+
const { onDispatchAction, onOptionsChange } = React.useContext(
86+
NavigationBuilderContext
87+
);
8488

8589
const context = React.useMemo(
8690
() => ({
@@ -91,6 +95,7 @@ export default function useDescriptors<
9195
addStateGetter,
9296
onRouteFocus,
9397
onDispatchAction,
98+
onOptionsChange,
9499
}),
95100
[
96101
addActionListener,
@@ -100,6 +105,7 @@ export default function useDescriptors<
100105
onAction,
101106
onDispatchAction,
102107
onRouteFocus,
108+
onOptionsChange,
103109
]
104110
);
105111

@@ -145,14 +151,18 @@ export default function useDescriptors<
145151
render() {
146152
return (
147153
<NavigationBuilderContext.Provider key={route.key} value={context}>
148-
<SceneView
149-
navigation={navigation}
150-
route={route}
151-
screen={screen}
152-
getState={getState}
153-
setState={setState}
154-
options={routeOptions}
155-
/>
154+
<NavigationContext.Provider value={navigation}>
155+
<NavigationRouteContext.Provider value={route}>
156+
<SceneView
157+
navigation={navigation}
158+
route={route}
159+
screen={screen}
160+
getState={getState}
161+
setState={setState}
162+
options={routeOptions}
163+
/>
164+
</NavigationRouteContext.Provider>
165+
</NavigationContext.Provider>
156166
</NavigationBuilderContext.Provider>
157167
);
158168
},

packages/core/src/useNavigationBuilder.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ export default function useNavigationBuilder<
293293
let state =
294294
// If the state isn't initialized, or stale, use the state we initialized instead
295295
// The state won't update until there's a change needed in the state we have initalized locally
296-
// So it'll be `undefined` or stale untill the first navigation event happens
296+
// So it'll be `undefined` or stale until the first navigation event happens
297297
isStateInitialized(currentState)
298298
? (currentState as State)
299299
: (initializedState as State);
@@ -344,7 +344,7 @@ export default function useNavigationBuilder<
344344

345345
// The up-to-date state will come in next render, but we don't need to wait for it
346346
// We can't use the outdated state since the screens have changed, which will cause error due to mismatched config
347-
// So we override the state objec we return to use the latest state as soon as possible
347+
// So we override the state object we return to use the latest state as soon as possible
348348
state = nextState;
349349

350350
React.useEffect(() => {

packages/core/src/useOptionsGetters.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ export default function useOptionsGetters({
1111
getOptions?: () => object | undefined;
1212
getState?: () => NavigationState;
1313
}) {
14+
let [
15+
numberOfChildrenListeners,
16+
setNumberOfChildrenListeners,
17+
] = React.useState(0);
1418
const optionsGettersFromChild = React.useRef<
1519
Record<string, (() => object | undefined | null) | undefined>
1620
>({});
@@ -55,16 +59,24 @@ export default function useOptionsGetters({
5559
const addOptionsGetter = React.useCallback(
5660
(key: string, getter: () => object | undefined | null) => {
5761
optionsGettersFromChild.current[key] = getter;
62+
setNumberOfChildrenListeners((prev) => prev + 1);
5863

5964
return () => {
65+
setNumberOfChildrenListeners((prev) => prev - 1);
6066
optionsGettersFromChild.current[key] = undefined;
6167
};
6268
},
6369
[]
6470
);
6571

72+
const hasAnyChildListener = React.useMemo(
73+
() => numberOfChildrenListeners > 0,
74+
[numberOfChildrenListeners]
75+
);
76+
6677
return {
6778
addOptionsGetter,
6879
getCurrentOptions,
80+
hasAnyChildListener,
6981
};
7082
}

0 commit comments

Comments
 (0)