mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-12 22:51:18 +08:00
feat: add a setOptions method to set screen options
In React Navigation, the screen options can be specified statically. If you need to configure any options based on props and state of the component, or want to update state and props based on some action such as tab press, you need to do it in a hacky way by changing params. it's way more complicated than it needs to be. It also breaks when used with HOCs which don't hoist static props, a common source of confusion. This PR adds a `setOptions` API to be able to update options directly without going through params.
This commit is contained in:
committed by
Satyajit Sahoo
parent
2b819e4310
commit
2eb86cd215
24
README.md
24
README.md
@@ -109,6 +109,30 @@ A render callback which doesn't have such limitation and is easier to use for th
|
||||
|
||||
The rendered component will receives a `navigation` prop with various helpers and a `route` prop which represents the route being rendered.
|
||||
|
||||
## Setting screen options
|
||||
|
||||
In React Navigation, screen options can be specified in a static property on the component (`navigationOptions`). This poses few issues:
|
||||
|
||||
- It's not possible to configure options based on props, state or context
|
||||
- To update the props based on an action in the component (such as button press), we need to do it in a hacky way by changing params
|
||||
- It breaks when used with HOCs which don't hoist static props, which is a common source of confusion
|
||||
|
||||
Instead of a static property, we expose a method to configure screen options:
|
||||
|
||||
```js
|
||||
function Selection({ navigation }) {
|
||||
const [selectedIds, setSelectedIds] = React.useState([]);
|
||||
|
||||
navigation.setOptions({
|
||||
title: `${selectedIds.length} items selected`,
|
||||
});
|
||||
|
||||
return <SelectionList onSelect={id => setSelectedIds(ids => [...ids, id])} />;
|
||||
}
|
||||
```
|
||||
|
||||
This allows options to be changed based on props, state or context, and doesn't have the disadvantages of static configuration.
|
||||
|
||||
## Type-checking
|
||||
|
||||
The library exports few helper types. Each navigator also need to export a custom type for the `navigation` prop which should contain the actions they provide, .e.g. `push` for stack, `jumpTo` for tab etc.
|
||||
|
||||
@@ -37,7 +37,7 @@ export type StackNavigationOptions = {
|
||||
|
||||
export type StackNavigationProp<
|
||||
ParamList extends ParamListBase
|
||||
> = NavigationProp<ParamList> & {
|
||||
> = NavigationProp<ParamList, StackNavigationOptions> & {
|
||||
/**
|
||||
* Push a new screen onto the stack.
|
||||
*
|
||||
|
||||
@@ -31,7 +31,8 @@ export type TabNavigationOptions = {
|
||||
};
|
||||
|
||||
export type TabNavigationProp<ParamList extends ParamListBase> = NavigationProp<
|
||||
ParamList
|
||||
ParamList,
|
||||
TabNavigationOptions
|
||||
> & {
|
||||
/**
|
||||
* Jump to an existing tab.
|
||||
|
||||
@@ -65,20 +65,34 @@ const Second = ({
|
||||
StackNavigationProp<StackParamList>,
|
||||
NavigationProp<TabParamList>
|
||||
>;
|
||||
}) => (
|
||||
<div>
|
||||
<h1>Second</h1>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigation.push('first', { author: 'Joel' })}
|
||||
>
|
||||
Push first
|
||||
</button>
|
||||
<button type="button" onClick={() => navigation.pop()}>
|
||||
Pop
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}) => {
|
||||
const [count, setCount] = React.useState(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
const timer = setInterval(() => setCount(c => c + 1), 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
navigation.setOptions({
|
||||
title: `Count ${count}`,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Second</h1>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigation.push('first', { author: 'Joel' })}
|
||||
>
|
||||
Push first
|
||||
</button>
|
||||
<button type="button" onClick={() => navigation.pop()}>
|
||||
Pop
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Fourth = ({
|
||||
navigation,
|
||||
|
||||
@@ -17,6 +17,9 @@ type Props = {
|
||||
route: Route & { state?: NavigationState };
|
||||
getState: () => NavigationState;
|
||||
setState: (state: NavigationState) => void;
|
||||
setOptions: (
|
||||
cb: (options: { [key: string]: object }) => { [key: string]: object }
|
||||
) => void;
|
||||
};
|
||||
|
||||
export default function SceneView({
|
||||
@@ -25,6 +28,7 @@ export default function SceneView({
|
||||
navigation: helpers,
|
||||
getState,
|
||||
setState,
|
||||
setOptions,
|
||||
}: Props) {
|
||||
const { performTransaction } = React.useContext(NavigationStateContext);
|
||||
|
||||
@@ -34,8 +38,16 @@ export default function SceneView({
|
||||
setParams: (params: object, target?: TargetRoute<string>) => {
|
||||
helpers.setParams(params, target ? target : { key: route.key });
|
||||
},
|
||||
setOptions: (options: object) =>
|
||||
setOptions(o => ({
|
||||
...o,
|
||||
[route.key]: {
|
||||
...o[route.key],
|
||||
...options,
|
||||
},
|
||||
})),
|
||||
}),
|
||||
[helpers, route.key]
|
||||
[helpers, route.key, setOptions]
|
||||
);
|
||||
|
||||
const getCurrentState = React.useCallback(() => {
|
||||
|
||||
218
src/__tests__/useDescriptors.test.tsx
Normal file
218
src/__tests__/useDescriptors.test.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import * as React from 'react';
|
||||
import { render, act } from 'react-native-testing-library';
|
||||
import useNavigationBuilder from '../useNavigationBuilder';
|
||||
import NavigationContainer from '../NavigationContainer';
|
||||
import Screen from '../Screen';
|
||||
import MockRouter from './__fixtures__/MockRouter';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
beforeEach(() => (MockRouter.key = 0));
|
||||
|
||||
it('sets options with options prop as an object', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
const { state, descriptors } = useNavigationBuilder<{ title?: string }>(
|
||||
MockRouter,
|
||||
props
|
||||
);
|
||||
const { render, options } = descriptors[state.routes[state.index].key];
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1>{options.title}</h1>
|
||||
<div>{render()}</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
const TestScreen = (): any => 'Test screen';
|
||||
|
||||
const root = render(
|
||||
<NavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen
|
||||
name="foo"
|
||||
component={TestScreen}
|
||||
options={{ title: 'Hello world' }}
|
||||
/>
|
||||
<Screen name="bar" component={jest.fn()} />
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
expect(root).toMatchInlineSnapshot(`
|
||||
<main>
|
||||
<h1>
|
||||
Hello world
|
||||
</h1>
|
||||
<div>
|
||||
Test screen
|
||||
</div>
|
||||
</main>
|
||||
`);
|
||||
});
|
||||
|
||||
it('sets options with options prop as a fuction', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
const { state, descriptors } = useNavigationBuilder<{ title?: string }>(
|
||||
MockRouter,
|
||||
props
|
||||
);
|
||||
const { render, options } = descriptors[state.routes[state.index].key];
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1>{options.title}</h1>
|
||||
<div>{render()}</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
const TestScreen = (): any => 'Test screen';
|
||||
|
||||
const root = render(
|
||||
<NavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen
|
||||
name="foo"
|
||||
component={TestScreen}
|
||||
options={({ route }: any) => ({ title: route.params.author })}
|
||||
initialParams={{ author: 'Jane' }}
|
||||
/>
|
||||
<Screen name="bar" component={jest.fn()} />
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
expect(root).toMatchInlineSnapshot(`
|
||||
<main>
|
||||
<h1>
|
||||
Jane
|
||||
</h1>
|
||||
<div>
|
||||
Test screen
|
||||
</div>
|
||||
</main>
|
||||
`);
|
||||
});
|
||||
|
||||
it('sets initial options with setOptions', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
const { state, descriptors } = useNavigationBuilder<{
|
||||
title?: string;
|
||||
color?: string;
|
||||
}>(MockRouter, props);
|
||||
const { render, options } = descriptors[state.routes[state.index].key];
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1 color={options.color}>{options.title}</h1>
|
||||
<div>{render()}</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
const TestScreen = ({ navigation }: any): any => {
|
||||
navigation.setOptions({
|
||||
title: 'Hello world',
|
||||
});
|
||||
|
||||
return 'Test screen';
|
||||
};
|
||||
|
||||
const root = render(
|
||||
<NavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="foo" options={{ color: 'blue' }}>
|
||||
{props => <TestScreen {...props} />}
|
||||
</Screen>
|
||||
<Screen name="bar" component={jest.fn()} />
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
expect(root).toMatchInlineSnapshot(`
|
||||
<main>
|
||||
<h1
|
||||
color="blue"
|
||||
>
|
||||
Hello world
|
||||
</h1>
|
||||
<div>
|
||||
Test screen
|
||||
</div>
|
||||
</main>
|
||||
`);
|
||||
});
|
||||
|
||||
it('updates options with setOptions', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
const { state, descriptors } = useNavigationBuilder<any>(MockRouter, props);
|
||||
const { render, options } = descriptors[state.routes[state.index].key];
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1 color={options.color}>{options.title}</h1>
|
||||
<p>{options.description}</p>
|
||||
<caption>{options.author}</caption>
|
||||
<div>{render()}</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
const TestScreen = ({ navigation }: any): any => {
|
||||
navigation.setOptions({
|
||||
title: 'Hello world',
|
||||
description: 'Something here',
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
const timer = setTimeout(() =>
|
||||
navigation.setOptions({
|
||||
title: 'Hello again',
|
||||
author: 'Jane',
|
||||
})
|
||||
);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
});
|
||||
|
||||
return 'Test screen';
|
||||
};
|
||||
|
||||
const element = (
|
||||
<NavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="foo" options={{ color: 'blue' }}>
|
||||
{props => <TestScreen {...props} />}
|
||||
</Screen>
|
||||
<Screen name="bar" component={jest.fn()} />
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
const root = render(element);
|
||||
|
||||
act(() => jest.runAllTimers());
|
||||
|
||||
root.update(element);
|
||||
|
||||
expect(root).toMatchInlineSnapshot(`
|
||||
<main>
|
||||
<h1
|
||||
color="blue"
|
||||
>
|
||||
Hello again
|
||||
</h1>
|
||||
<p>
|
||||
Something here
|
||||
</p>
|
||||
<caption>
|
||||
Jane
|
||||
</caption>
|
||||
<div>
|
||||
Test screen
|
||||
</div>
|
||||
</main>
|
||||
`);
|
||||
});
|
||||
@@ -3,18 +3,18 @@ import { ParamListBase, RouteConfig, TypedNavigator } from './types';
|
||||
import Screen from './Screen';
|
||||
|
||||
export default function createNavigator<
|
||||
Options extends object,
|
||||
ScreenOptions extends object,
|
||||
N extends React.ComponentType<any>
|
||||
>(RawNavigator: N) {
|
||||
return function Navigator<ParamList extends ParamListBase>(): TypedNavigator<
|
||||
ParamList,
|
||||
Options,
|
||||
ScreenOptions,
|
||||
typeof RawNavigator
|
||||
> {
|
||||
return {
|
||||
Navigator: RawNavigator,
|
||||
Screen: Screen as React.ComponentType<
|
||||
RouteConfig<ParamList, keyof ParamList, Options>
|
||||
RouteConfig<ParamList, keyof ParamList, ScreenOptions>
|
||||
>,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -149,7 +149,10 @@ class PrivateValueStore<T> {
|
||||
private __private_value_type?: T;
|
||||
}
|
||||
|
||||
export type NavigationProp<ParamList extends ParamListBase = ParamListBase> = {
|
||||
export type NavigationProp<
|
||||
ParamList extends ParamListBase = ParamListBase,
|
||||
ScreenOptions extends object = object
|
||||
> = {
|
||||
/**
|
||||
* Dispatch an action or an update function to the router.
|
||||
* The update function will receive the current state,
|
||||
@@ -208,6 +211,14 @@ export type NavigationProp<ParamList extends ParamListBase = ParamListBase> = {
|
||||
params: ParamList[RouteName],
|
||||
target: TargetRoute<RouteName>
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Update the options for the route.
|
||||
* The options object will be shallow merged with default options object.
|
||||
*
|
||||
* @param options Options object for the route.
|
||||
*/
|
||||
setOptions(options: Partial<ScreenOptions>): void;
|
||||
} & PrivateValueStore<ParamList>;
|
||||
|
||||
export type RouteProp<
|
||||
@@ -224,15 +235,17 @@ export type RouteProp<
|
||||
});
|
||||
|
||||
export type CompositeNavigationProp<
|
||||
A extends NavigationProp<ParamListBase>,
|
||||
B extends NavigationProp<ParamListBase>
|
||||
> = Omit<A & B, keyof NavigationProp<any>> &
|
||||
A extends NavigationProp<ParamListBase, object>,
|
||||
B extends NavigationProp<ParamListBase, object>
|
||||
> = Omit<A & B, keyof NavigationProp<any, any>> &
|
||||
NavigationProp<
|
||||
(A extends NavigationProp<infer T> ? T : never) &
|
||||
(B extends NavigationProp<infer U> ? U : never)
|
||||
(A extends NavigationProp<infer T, any> ? T : never) &
|
||||
(B extends NavigationProp<infer U, any> ? U : never),
|
||||
(A extends NavigationProp<any, infer O> ? O : never) &
|
||||
(B extends NavigationProp<any, infer P> ? P : never)
|
||||
>;
|
||||
|
||||
export type Descriptor<Options extends object> = {
|
||||
export type Descriptor<ScreenOptions extends object> = {
|
||||
/**
|
||||
* Render the component associated with this route.
|
||||
*/
|
||||
@@ -241,13 +254,13 @@ export type Descriptor<Options extends object> = {
|
||||
/**
|
||||
* Options for the route.
|
||||
*/
|
||||
options: Options;
|
||||
options: ScreenOptions;
|
||||
};
|
||||
|
||||
export type RouteConfig<
|
||||
ParamList extends ParamListBase = ParamListBase,
|
||||
RouteName extends keyof ParamList = string,
|
||||
Options extends object = object
|
||||
ScreenOptions extends object = object
|
||||
> = {
|
||||
/**
|
||||
* Route name of this screen.
|
||||
@@ -258,11 +271,11 @@ export type RouteConfig<
|
||||
* Navigator options for this screen.
|
||||
*/
|
||||
options?:
|
||||
| Options
|
||||
| ScreenOptions
|
||||
| ((props: {
|
||||
route: RouteProp<ParamList, RouteName>;
|
||||
navigation: NavigationProp<ParamList>;
|
||||
}) => Options);
|
||||
}) => ScreenOptions);
|
||||
|
||||
/**
|
||||
* Initial params object for the route.
|
||||
@@ -284,7 +297,7 @@ export type RouteConfig<
|
||||
|
||||
export type TypedNavigator<
|
||||
ParamList extends ParamListBase,
|
||||
Options extends object,
|
||||
ScreenOptions extends object,
|
||||
Navigator extends React.ComponentType<any>
|
||||
> = {
|
||||
Navigator: React.ComponentType<
|
||||
@@ -295,5 +308,7 @@ export type TypedNavigator<
|
||||
initialRouteName?: keyof ParamList;
|
||||
}
|
||||
>;
|
||||
Screen: React.ComponentType<RouteConfig<ParamList, keyof ParamList, Options>>;
|
||||
Screen: React.ComponentType<
|
||||
RouteConfig<ParamList, keyof ParamList, ScreenOptions>
|
||||
>;
|
||||
};
|
||||
|
||||
@@ -36,6 +36,7 @@ export default function useDescriptors<ScreenOptions extends object>({
|
||||
removeActionListener,
|
||||
onRouteFocus,
|
||||
}: Options) {
|
||||
const [options, setOptions] = React.useState<{ [key: string]: object }>({});
|
||||
const context = React.useMemo(
|
||||
() => ({
|
||||
navigation,
|
||||
@@ -67,6 +68,7 @@ export default function useDescriptors<ScreenOptions extends object>({
|
||||
screen={screen}
|
||||
getState={getState}
|
||||
setState={setState}
|
||||
setOptions={setOptions}
|
||||
/>
|
||||
</NavigationBuilderContext.Provider>
|
||||
);
|
||||
@@ -79,6 +81,7 @@ export default function useDescriptors<ScreenOptions extends object>({
|
||||
navigation,
|
||||
})
|
||||
: screen.options),
|
||||
...options[route.key],
|
||||
},
|
||||
};
|
||||
return acc;
|
||||
|
||||
Reference in New Issue
Block a user