diff --git a/README.md b/README.md index 5fcfd594..4d906f37 100644 --- a/README.md +++ b/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 hook to configure screen options: + +```js +function Selection() { + const [selectedIds, setSelectedIds] = React.useState([]); + + useOptions({ + title: `${selectedIds.length} items selected`, + }); + + return 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. @@ -182,4 +206,16 @@ And then we can use it: ``` +To provide type-checking in the `useOptions` hook, we can provide it a type parameter: + +```ts +function Profile({ userId }: Props) { + useOptions({ + title: `${userId}'s profile`, + }); + + // Content +} +``` + Unfortunately it's not possible to verify that the type of children elements are correct since [TypeScript doesn't support type-checking JSX elements](https://github.com/microsoft/TypeScript/issues/21699). diff --git a/example/index.tsx b/example/index.tsx index 1d5432af..331901bc 100644 --- a/example/index.tsx +++ b/example/index.tsx @@ -6,8 +6,12 @@ import { PartialState, NavigationProp, RouteProp, + useOptions, } from '../src'; -import StackNavigator, { StackNavigationProp } from './StackNavigator'; +import StackNavigator, { + StackNavigationProp, + StackNavigationOptions, +} from './StackNavigator'; import TabNavigator, { TabNavigationProp } from './TabNavigator'; type StackParamList = { @@ -65,20 +69,34 @@ const Second = ({ StackNavigationProp, NavigationProp >; -}) => ( -
-

Second

- - -
-); +}) => { + const [count, setCount] = React.useState(0); + + React.useEffect(() => { + const timer = setInterval(() => setCount(c => c + 1), 1000); + + return () => clearInterval(timer); + }, []); + + useOptions({ + title: `Count ${count}`, + }); + + return ( +
+

Second

+ + +
+ ); +}; const Fourth = ({ navigation, diff --git a/src/SceneView.tsx b/src/SceneView.tsx index 01e29663..70aaf0e3 100644 --- a/src/SceneView.tsx +++ b/src/SceneView.tsx @@ -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) => { 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(() => { diff --git a/src/index.tsx b/src/index.tsx index 538ba80e..61ae27ef 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,5 +4,6 @@ export { default as createNavigator } from './createNavigator'; export { default as useNavigationBuilder } from './useNavigationBuilder'; export { default as useNavigation } from './useNavigation'; +export { default as useOptions } from './useOptions'; export * from './types'; diff --git a/src/types.tsx b/src/types.tsx index 235526f2..accf9061 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -208,6 +208,14 @@ export type NavigationProp = { params: ParamList[RouteName], target: TargetRoute ): 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: Options): void; } & PrivateValueStore; export type RouteProp< diff --git a/src/useDescriptors.tsx b/src/useDescriptors.tsx index 74fccd6e..47865ca0 100644 --- a/src/useDescriptors.tsx +++ b/src/useDescriptors.tsx @@ -36,6 +36,7 @@ export default function useDescriptors({ removeActionListener, onRouteFocus, }: Options) { + const [options, setOptions] = React.useState<{ [key: string]: object }>({}); const context = React.useMemo( () => ({ navigation, @@ -67,6 +68,7 @@ export default function useDescriptors({ screen={screen} getState={getState} setState={setState} + setOptions={setOptions} /> ); @@ -79,6 +81,7 @@ export default function useDescriptors({ navigation, }) : screen.options), + ...options[route.key], }, }; return acc; diff --git a/src/useOptions.tsx b/src/useOptions.tsx new file mode 100644 index 00000000..12b34ea0 --- /dev/null +++ b/src/useOptions.tsx @@ -0,0 +1,9 @@ +import useNavigation from './useNavigation'; + +export default function useOptions( + options: Options +) { + const navigation = useNavigation(); + + navigation.setOptions(options); +}