Skip to content
This repository has been archived by the owner on Feb 8, 2020. It is now read-only.

Commit

Permalink
feat: add a setOptions function and useOptions hook
Browse files Browse the repository at this point in the history
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, and an `useOptions` hook to make it easier to set them.
  • Loading branch information
satya164 committed Jul 21, 2019
1 parent 51200ba commit 61dbbd7
Show file tree
Hide file tree
Showing 11 changed files with 175 additions and 52 deletions.
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <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.
Expand Down Expand Up @@ -173,9 +197,25 @@ And then we can use it:
```js
<Stack.Navigator initialRouteName="profile">
<Stack.Screen name="settings" component={Settings} />
<Stack.Screen name="profile" component={Profile} />
<Stack.Screen
name="profile"
component={Profile}
options={{ title: 'My profile' }}
/>
<Stack.Screen name="home" component={Home} />
</Stack.Navigator>
```

To provide type-checking in the `useOptions` hook, we can provide it a type parameter:

```ts
function Profile({ userId }: Props) {
useOptions<StackNavigationOptions>({
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).
16 changes: 14 additions & 2 deletions example/StackNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ParamList> & {
Expand Down Expand Up @@ -236,7 +243,10 @@ const StackRouter: Router<CommonAction | Action> = {
};

export function StackNavigator(props: Props) {
const { state, descriptors } = useNavigationBuilder(StackRouter, props);
const { state, descriptors } = useNavigationBuilder<StackNavigationOptions>(
StackRouter,
props
);

return (
<div style={{ position: 'relative' }}>
Expand Down Expand Up @@ -277,4 +287,6 @@ export function StackNavigator(props: Props) {
);
}

export default createNavigator(StackNavigator);
export default createNavigator<StackNavigationOptions, typeof StackNavigator>(
StackNavigator
);
11 changes: 10 additions & 1 deletion example/TabNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ParamList extends ParamListBase> = NavigationProp<
ParamList
> & {
Expand Down Expand Up @@ -196,4 +203,6 @@ export function TabNavigator(props: Props) {
);
}

export default createNavigator(TabNavigator);
export default createNavigator<TabNavigationOptions, typeof TabNavigator>(
TabNavigator
);
60 changes: 39 additions & 21 deletions example/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -65,20 +69,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);
}, []);

useOptions<StackNavigationOptions>({
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,
Expand Down Expand Up @@ -149,14 +167,14 @@ function App() {
<MyStack.Screen
name="first"
component={First}
options={{ title: 'Foo' }}
options={({ route }) => ({
title: `Foo (${route.params ? route.params.author : ''})`,
})}
initialParams={{ author: 'Jane' }}
/>
<MyStack.Screen
name="second"
component={Second}
options={{ title: 'Bar' }}
/>
<MyStack.Screen name="second" options={{ title: 'Bar' }}>
{props => <Second {...props} />}
</MyStack.Screen>
<MyStack.Screen name="third" options={{ title: 'Baz' }}>
{() => (
<MyTab.Navigator initialRouteName="fifth">
Expand Down
23 changes: 20 additions & 3 deletions src/SceneView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -29,8 +38,16 @@ export default function SceneView(props: Props) {
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(() => {
Expand Down
10 changes: 6 additions & 4 deletions src/createNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@ import * as React from 'react';
import { ParamListBase, RouteConfig, TypedNavigator } from './types';
import Screen from './Screen';

export default function createNavigator<N extends React.ComponentType<any>>(
RawNavigator: N
) {
export default function createNavigator<
Options extends object,
N extends React.ComponentType<any>
>(RawNavigator: N) {
return function Navigator<ParamList extends ParamListBase>(): TypedNavigator<
ParamList,
Options,
typeof RawNavigator
> {
return {
Navigator: RawNavigator,
Screen: Screen as React.ComponentType<
RouteConfig<ParamList, keyof ParamList>
RouteConfig<ParamList, keyof ParamList, Options>
>,
};
};
Expand Down
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
32 changes: 19 additions & 13 deletions src/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,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 extends object = object>(options: Options): void;
} & PrivateValueStore<ParamList>;

export type RouteProp<
Expand All @@ -232,7 +240,7 @@ export type CompositeNavigationProp<
(B extends NavigationProp<infer U> ? U : never)
>;

export type Descriptor = {
export type Descriptor<Options extends object> = {
/**
* Render the component associated with this route.
*/
Expand All @@ -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.
Expand All @@ -265,7 +265,12 @@ export type RouteConfig<
/**
* Navigator options for this screen.
*/
options?: Options;
options?:
| Options
| ((props: {
route: RouteProp<ParamList, RouteName>;
navigation: NavigationProp<ParamList>;
}) => Options);

/**
* Initial params object for the route.
Expand All @@ -287,6 +292,7 @@ export type RouteConfig<

export type TypedNavigator<
ParamList extends ParamListBase,
Options extends object,
Navigator extends React.ComponentType<any>
> = {
Navigator: React.ComponentType<
Expand All @@ -297,5 +303,5 @@ export type TypedNavigator<
initialRouteName?: keyof ParamList;
}
>;
Screen: React.ComponentType<RouteConfig<ParamList, keyof ParamList>>;
Screen: React.ComponentType<RouteConfig<ParamList, keyof ParamList, Options>>;
};
19 changes: 14 additions & 5 deletions src/useDescriptors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ type Options = {
onRouteFocus: (key: string) => void;
};

const EMPTY_OPTIONS = Object.freeze({});

export default function useDescriptors({
export default function useDescriptors<ScreenOptions extends object>({
state,
screens,
navigation,
Expand All @@ -38,6 +36,7 @@ export default function useDescriptors({
removeActionListener,
onRouteFocus,
}: Options) {
const [options, setOptions] = React.useState<{ [key: string]: object }>({});
const context = React.useMemo(
() => ({
navigation,
Expand Down Expand Up @@ -69,14 +68,24 @@ export default function useDescriptors({
screen={screen}
getState={getState}
setState={setState}
setOptions={setOptions}
/>
</NavigationBuilderContext.Provider>
);
},
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<ScreenOptions> }
);
}
Loading

0 comments on commit 61dbbd7

Please sign in to comment.