diff --git a/README.md b/README.md index 5fcfd594..8398a226 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 method to configure screen options: + +```js +function Selection({ navigation }) { + const [selectedIds, setSelectedIds] = React.useState([]); + + navigation.setOptions({ + 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. diff --git a/example/StackNavigator.tsx b/example/StackNavigator.tsx index ac1cacbe..f2fa5a1d 100644 --- a/example/StackNavigator.tsx +++ b/example/StackNavigator.tsx @@ -37,7 +37,7 @@ export type StackNavigationOptions = { export type StackNavigationProp< ParamList extends ParamListBase -> = NavigationProp & { +> = NavigationProp & { /** * Push a new screen onto the stack. * diff --git a/example/TabNavigator.tsx b/example/TabNavigator.tsx index aaf6e673..2452987d 100644 --- a/example/TabNavigator.tsx +++ b/example/TabNavigator.tsx @@ -28,10 +28,15 @@ export type TabNavigationOptions = { * Title text for the screen. */ title?: string; + /** + * Stuff + */ + label: number; }; export type TabNavigationProp = NavigationProp< - ParamList + ParamList, + TabNavigationOptions > & { /** * Jump to an existing tab. diff --git a/example/index.tsx b/example/index.tsx index 1d5432af..4eec30f6 100644 --- a/example/index.tsx +++ b/example/index.tsx @@ -65,20 +65,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); + }, []); + + navigation.setOptions({ + 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/createNavigator.tsx b/src/createNavigator.tsx index 152a0d5e..ce6c270d 100644 --- a/src/createNavigator.tsx +++ b/src/createNavigator.tsx @@ -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 >(RawNavigator: N) { return function Navigator(): TypedNavigator< ParamList, - Options, + ScreenOptions, typeof RawNavigator > { return { Navigator: RawNavigator, Screen: Screen as React.ComponentType< - RouteConfig + RouteConfig >, }; }; diff --git a/src/types.tsx b/src/types.tsx index 235526f2..79ce2561 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -149,7 +149,10 @@ class PrivateValueStore { private __private_value_type?: T; } -export type NavigationProp = { +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 = { 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: Partial): void; } & PrivateValueStore; export type RouteProp< @@ -224,15 +235,17 @@ export type RouteProp< }); export type CompositeNavigationProp< - A extends NavigationProp, - B extends NavigationProp -> = Omit> & + A extends NavigationProp, + B extends NavigationProp +> = Omit> & NavigationProp< - (A extends NavigationProp ? T : never) & - (B extends NavigationProp ? U : never) + (A extends NavigationProp ? T : never) & + (B extends NavigationProp ? U : never), + (A extends NavigationProp ? O : never) & + (B extends NavigationProp ? P : never) >; -export type Descriptor = { +export type Descriptor = { /** * Render the component associated with this route. */ @@ -241,13 +254,13 @@ export type Descriptor = { /** * 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; navigation: NavigationProp; - }) => 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 > = { Navigator: React.ComponentType< @@ -295,5 +308,7 @@ export type TypedNavigator< initialRouteName?: keyof ParamList; } >; - Screen: React.ComponentType>; + Screen: React.ComponentType< + RouteConfig + >; }; 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;