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 method to set screen options
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.
  • Loading branch information
satya164 committed Jul 22, 2019
1 parent 62f4047 commit cbfbf4e
Show file tree
Hide file tree
Showing 9 changed files with 320 additions and 33 deletions.
24 changes: 24 additions & 0 deletions 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 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.
Expand Down
2 changes: 1 addition & 1 deletion example/StackNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
3 changes: 2 additions & 1 deletion example/TabNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export type TabNavigationOptions = {
};

export type TabNavigationProp<ParamList extends ParamListBase> = NavigationProp<
ParamList
ParamList,
TabNavigationOptions
> & {
/**
* Jump to an existing tab.
Expand Down
42 changes: 28 additions & 14 deletions example/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 13 additions & 1 deletion src/SceneView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -25,6 +28,7 @@ export default function SceneView({
navigation: helpers,
getState,
setState,
setOptions,
}: Props) {
const { performTransaction } = React.useContext(NavigationStateContext);

Expand All @@ -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(() => {
Expand Down
218 changes: 218 additions & 0 deletions src/__tests__/useDescriptors.test.tsx
Original file line number Diff line number Diff line change
@@ -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>
`);
});
6 changes: 3 additions & 3 deletions src/createNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
>,
};
};
Expand Down
Loading

0 comments on commit cbfbf4e

Please sign in to comment.