diff --git a/README.md b/README.md index 11bccaa8..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. @@ -173,9 +197,25 @@ And then we can use it: ```js - + ``` +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/StackNavigator.tsx b/example/StackNavigator.tsx index 6466330a..ac1cacbe 100644 --- a/example/StackNavigator.tsx +++ b/example/StackNavigator.tsx @@ -28,6 +28,13 @@ type Action = } | { type: 'POP_TO_TOP' }; +export type StackNavigationOptions = { + /** + * Title text for the screen. + */ + title?: string; +}; + export type StackNavigationProp< ParamList extends ParamListBase > = NavigationProp & { @@ -236,7 +243,10 @@ const StackRouter: Router = { }; export function StackNavigator(props: Props) { - const { state, descriptors } = useNavigationBuilder(StackRouter, props); + const { state, descriptors } = useNavigationBuilder( + StackRouter, + props + ); return (
@@ -277,4 +287,6 @@ export function StackNavigator(props: Props) { ); } -export default createNavigator(StackNavigator); +export default createNavigator( + StackNavigator +); diff --git a/example/TabNavigator.tsx b/example/TabNavigator.tsx index f7afcefa..aaf6e673 100644 --- a/example/TabNavigator.tsx +++ b/example/TabNavigator.tsx @@ -23,6 +23,13 @@ type Action = { payload: { name?: string; key?: string; params?: object }; }; +export type TabNavigationOptions = { + /** + * Title text for the screen. + */ + title?: string; +}; + export type TabNavigationProp = NavigationProp< ParamList > & { @@ -196,4 +203,6 @@ export function TabNavigator(props: Props) { ); } -export default createNavigator(TabNavigator); +export default createNavigator( + TabNavigator +); diff --git a/example/index.tsx b/example/index.tsx index 34f5dd62..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, @@ -149,14 +167,14 @@ function App() { ({ + title: `Foo (${route.params ? route.params.author : ''})`, + })} initialParams={{ author: 'Jane' }} /> - + + {props => } + {() => ( diff --git a/src/SceneView.tsx b/src/SceneView.tsx index f320ce29..70aaf0e3 100644 --- a/src/SceneView.tsx +++ b/src/SceneView.tsx @@ -17,10 +17,19 @@ 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(props: Props) { - const { screen, route, navigation: helpers, getState, setState } = props; +export default function SceneView({ + screen, + route, + navigation: helpers, + getState, + setState, + setOptions, +}: Props) { const { performTransaction } = React.useContext(NavigationStateContext); const navigation = React.useMemo( @@ -29,8 +38,16 @@ export default function SceneView(props: Props) { 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 0276f47e..152a0d5e 100644 --- a/src/createNavigator.tsx +++ b/src/createNavigator.tsx @@ -2,17 +2,19 @@ import * as React from 'react'; import { ParamListBase, RouteConfig, TypedNavigator } from './types'; import Screen from './Screen'; -export default function createNavigator>( - RawNavigator: N -) { +export default function createNavigator< + Options extends object, + N extends React.ComponentType +>(RawNavigator: N) { return function Navigator(): TypedNavigator< ParamList, + Options, typeof RawNavigator > { return { Navigator: RawNavigator, Screen: Screen as React.ComponentType< - RouteConfig + RouteConfig >, }; }; 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 ee45155a..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< @@ -232,7 +240,7 @@ export type CompositeNavigationProp< (B extends NavigationProp ? U : never) >; -export type Descriptor = { +export type Descriptor = { /** * Render the component associated with this route. */ @@ -244,18 +252,10 @@ export type Descriptor = { options: Options; }; -export type Options = { - /** - * Title text for the screen. - */ - title?: string; - - [key: string]: any; -}; - export type RouteConfig< ParamList extends ParamListBase = ParamListBase, - RouteName extends keyof ParamList = string + RouteName extends keyof ParamList = string, + Options extends object = object > = { /** * Route name of this screen. @@ -265,7 +265,12 @@ export type RouteConfig< /** * Navigator options for this screen. */ - options?: Options; + options?: + | Options + | ((props: { + route: RouteProp; + navigation: NavigationProp; + }) => Options); /** * Initial params object for the route. @@ -287,6 +292,7 @@ export type RouteConfig< export type TypedNavigator< ParamList extends ParamListBase, + Options extends object, Navigator extends React.ComponentType > = { Navigator: React.ComponentType< @@ -297,5 +303,5 @@ export type TypedNavigator< initialRouteName?: keyof ParamList; } >; - Screen: React.ComponentType>; + Screen: React.ComponentType>; }; diff --git a/src/useDescriptors.tsx b/src/useDescriptors.tsx index d10fedc2..47865ca0 100644 --- a/src/useDescriptors.tsx +++ b/src/useDescriptors.tsx @@ -25,9 +25,7 @@ type Options = { onRouteFocus: (key: string) => void; }; -const EMPTY_OPTIONS = Object.freeze({}); - -export default function useDescriptors({ +export default function useDescriptors({ state, screens, navigation, @@ -38,6 +36,7 @@ export default function useDescriptors({ removeActionListener, onRouteFocus, }: Options) { + const [options, setOptions] = React.useState<{ [key: string]: object }>({}); const context = React.useMemo( () => ({ navigation, @@ -69,14 +68,24 @@ export default function useDescriptors({ screen={screen} getState={getState} setState={setState} + setOptions={setOptions} /> ); }, - options: screen.options || EMPTY_OPTIONS, + options: { + ...(typeof screen.options === 'function' + ? screen.options({ + // @ts-ignore + route, + navigation, + }) + : screen.options), + ...options[route.key], + }, }; return acc; }, - {} as { [key: string]: Descriptor } + {} as { [key: string]: Descriptor } ); } diff --git a/src/useNavigationBuilder.tsx b/src/useNavigationBuilder.tsx index 2bbacb0e..8f6e89c0 100644 --- a/src/useNavigationBuilder.tsx +++ b/src/useNavigationBuilder.tsx @@ -39,7 +39,7 @@ const getRouteConfigsFromChildren = (children: React.ReactNode) => ); }, []); -export default function useNavigationBuilder( +export default function useNavigationBuilder( router: Router, options: Options ) { @@ -156,7 +156,7 @@ export default function useNavigationBuilder( actionCreators: router.actionCreators, }); - const descriptors = useDescriptors({ + const descriptors = useDescriptors({ state, screens, navigation, 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); +}