mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-12 22:51:18 +08:00
feat: add a useFocusEffect hook
This commit is contained in:
committed by
Satyajit Sahoo
parent
fb8d3024bf
commit
819b7904fa
49
README.md
49
README.md
@@ -66,8 +66,8 @@ If an initial state is specified, e.g. as a result of `Linking.getInitialURL()`,
|
||||
## Basic usage
|
||||
|
||||
```js
|
||||
const Stack = StackNavigator();
|
||||
const Tab = TabNavigator();
|
||||
const Stack = createStackNavigator();
|
||||
const Tab = createTabNavigator();
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@@ -163,6 +163,45 @@ The `target` property determines the screen that will receive the event. If the
|
||||
|
||||
Screens cannot emit events as there is no `emit` method on a screen's `navigation` prop.
|
||||
|
||||
If you don't need to get notified of focus change, but just need to check if the screen is currently focused in a callback, you can use the `navigation.isFocused()` method which returns a boolean. Note that it's not safe to use this in `render`. Only use it in callbacks, event listeners etc.
|
||||
|
||||
## Side-effects in focused screen
|
||||
|
||||
Sometimes we want to run side-effects when a screen is focused. A side effect may involve things like adding an event listener, fetching data, updating document title, etc. While this can be achieved using `focus` and `blur` events, it's not very ergonomic.
|
||||
|
||||
To make this easier, the library exports a `useFocusEffect` hook:
|
||||
|
||||
```js
|
||||
function Profile({ userId }) {
|
||||
const [user, setUser] = React.useState(null);
|
||||
|
||||
const fetchUser = React.useCallback(() => {
|
||||
const request = API.fetchUser(userId).then(
|
||||
data => setUser(data),
|
||||
error => alert(error.message)
|
||||
);
|
||||
|
||||
return () => request.abort();
|
||||
}, [userId]);
|
||||
|
||||
useFocusEffect(fetchUser);
|
||||
|
||||
return <ProfileContent user={user} />;
|
||||
}
|
||||
```
|
||||
|
||||
The `useFocusEffect` is analogous to React's `useEffect` hook. The only difference is that it runs on focus instead of render.
|
||||
|
||||
**NOTE:** To avoid the running the effect too often, it's important to wrap the callback in `useCallback` before passing it to `useFocusEffect` as shown in the example.
|
||||
|
||||
## Access navigation anywhere
|
||||
|
||||
Passing the `navigation` prop down can be tedious. The library exports a `useNavigation` hook which can access the `navigation` prop from the parent screen.
|
||||
|
||||
```js
|
||||
const navigation = useNavigation();
|
||||
```
|
||||
|
||||
## 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.
|
||||
@@ -223,6 +262,12 @@ type FeedScreenNavigationProp = CompositeNavigationProp<
|
||||
>;
|
||||
```
|
||||
|
||||
To annotate the `navigation` prop from `useNavigation`, we can use a type parameter:
|
||||
|
||||
```ts
|
||||
const navigation = useNavigation<FeedScreenNavigationProp>();
|
||||
```
|
||||
|
||||
It's also possible to type-check the navigator to some extent. To do this, we need to pass a generic when creating the navigator object:
|
||||
|
||||
```ts
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
NavigationHelpers,
|
||||
RouteProp,
|
||||
InitialState,
|
||||
useFocusEffect,
|
||||
} from '../src';
|
||||
import createStackNavigator, { StackNavigationProp } from './StackNavigator';
|
||||
import createTabNavigator, { TabNavigationProp } from './TabNavigator';
|
||||
@@ -34,29 +35,39 @@ const First = ({
|
||||
NavigationHelpers<TabParamList>
|
||||
>;
|
||||
route: RouteProp<StackParamList, 'first'>;
|
||||
}) => (
|
||||
<div>
|
||||
<h1>First, {route.params.author}</h1>
|
||||
<button type="button" onClick={() => navigation.push('second')}>
|
||||
Push second
|
||||
</button>
|
||||
<button type="button" onClick={() => navigation.push('third')}>
|
||||
Push third
|
||||
</button>
|
||||
<button type="button" onClick={() => navigation.navigate('fourth')}>
|
||||
Navigate to fourth
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigation.navigate('first', { author: 'John' })}
|
||||
>
|
||||
Navigate with params
|
||||
</button>
|
||||
<button type="button" onClick={() => navigation.pop()}>
|
||||
Pop
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}) => {
|
||||
const updateTitle = React.useCallback(() => {
|
||||
document.title = `${route.name} (${route.params.author})`;
|
||||
|
||||
return () => (document.title = '');
|
||||
}, [route.name, route.params.author]);
|
||||
|
||||
useFocusEffect(updateTitle);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>First, {route.params.author}</h1>
|
||||
<button type="button" onClick={() => navigation.push('second')}>
|
||||
Push second
|
||||
</button>
|
||||
<button type="button" onClick={() => navigation.push('third')}>
|
||||
Push third
|
||||
</button>
|
||||
<button type="button" onClick={() => navigation.navigate('fourth')}>
|
||||
Navigate to fourth
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigation.navigate('first', { author: 'John' })}
|
||||
>
|
||||
Navigate with params
|
||||
</button>
|
||||
<button type="button" onClick={() => navigation.pop()}>
|
||||
Pop
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Second = ({
|
||||
navigation,
|
||||
|
||||
241
src/__tests__/useFocusEffect.test.tsx
Normal file
241
src/__tests__/useFocusEffect.test.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
import * as React from 'react';
|
||||
import { render, act } from 'react-native-testing-library';
|
||||
import useNavigationBuilder from '../useNavigationBuilder';
|
||||
import useFocusEffect from '../useFocusEffect';
|
||||
import NavigationContainer from '../NavigationContainer';
|
||||
import Screen from '../Screen';
|
||||
import MockRouter from './__fixtures__/MockRouter';
|
||||
|
||||
it('runs focus effect on focus change', () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
};
|
||||
|
||||
const focusEffect = jest.fn();
|
||||
const focusEffectCleanup = jest.fn();
|
||||
|
||||
const Test = () => {
|
||||
const onFocus = React.useCallback(() => {
|
||||
focusEffect();
|
||||
|
||||
return focusEffectCleanup;
|
||||
}, []);
|
||||
|
||||
useFocusEffect(onFocus);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const navigation = React.createRef<any>();
|
||||
|
||||
const element = (
|
||||
<NavigationContainer ref={navigation}>
|
||||
<TestNavigator>
|
||||
<Screen name="first">{() => null}</Screen>
|
||||
<Screen name="second" component={Test} />
|
||||
<Screen name="third">{() => null}</Screen>
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
render(element);
|
||||
|
||||
expect(focusEffect).not.toBeCalled();
|
||||
expect(focusEffectCleanup).not.toBeCalled();
|
||||
|
||||
act(() => navigation.current.navigate('second'));
|
||||
|
||||
expect(focusEffect).toBeCalledTimes(1);
|
||||
expect(focusEffectCleanup).not.toBeCalled();
|
||||
|
||||
act(() => navigation.current.navigate('third'));
|
||||
|
||||
expect(focusEffect).toBeCalledTimes(1);
|
||||
expect(focusEffectCleanup).toBeCalledTimes(1);
|
||||
|
||||
act(() => navigation.current.navigate('second'));
|
||||
|
||||
expect(focusEffect).toBeCalledTimes(2);
|
||||
expect(focusEffectCleanup).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('runs focus effect on deps change', () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return descriptors[state.routes[state.index].key].render();
|
||||
};
|
||||
|
||||
const focusEffect = jest.fn();
|
||||
const focusEffectCleanup = jest.fn();
|
||||
|
||||
const Test = ({ count }: { count: number }) => {
|
||||
const onFocus = React.useCallback(() => {
|
||||
focusEffect(count);
|
||||
|
||||
return focusEffectCleanup;
|
||||
}, [count]);
|
||||
|
||||
useFocusEffect(onFocus);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const App = ({ count }: { count: number }) => (
|
||||
<NavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="first">{() => <Test count={count} />}</Screen>
|
||||
<Screen name="second">{() => null}</Screen>
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
const root = render(<App count={1} />);
|
||||
|
||||
expect(focusEffect).toBeCalledTimes(1);
|
||||
expect(focusEffectCleanup).not.toBeCalled();
|
||||
|
||||
root.update(<App count={2} />);
|
||||
|
||||
expect(focusEffectCleanup).toBeCalledTimes(1);
|
||||
expect(focusEffect).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('runs focus effect when initial state is given', () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
};
|
||||
|
||||
const focusEffect = jest.fn();
|
||||
const focusEffectCleanup = jest.fn();
|
||||
|
||||
const Test = () => {
|
||||
useFocusEffect(() => {
|
||||
focusEffect();
|
||||
|
||||
return focusEffectCleanup;
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
index: 2,
|
||||
routes: [
|
||||
{ key: 'first', name: 'first' },
|
||||
{ key: 'second', name: 'second' },
|
||||
{ key: 'third', name: 'third' },
|
||||
],
|
||||
};
|
||||
|
||||
const navigation = React.createRef<any>();
|
||||
|
||||
const element = (
|
||||
<NavigationContainer ref={navigation} initialState={initialState}>
|
||||
<TestNavigator>
|
||||
<Screen name="first">{() => null}</Screen>
|
||||
<Screen name="second">{() => null}</Screen>
|
||||
<Screen name="third" component={Test} />
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
render(element);
|
||||
|
||||
expect(focusEffect).toBeCalledTimes(1);
|
||||
expect(focusEffectCleanup).not.toBeCalled();
|
||||
|
||||
act(() => navigation.current.navigate('first'));
|
||||
|
||||
expect(focusEffect).toBeCalledTimes(1);
|
||||
expect(focusEffectCleanup).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('runs focus effect when only focused route is rendered', () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return descriptors[state.routes[state.index].key].render();
|
||||
};
|
||||
|
||||
const focusEffect = jest.fn();
|
||||
const focusEffectCleanup = jest.fn();
|
||||
|
||||
const Test = () => {
|
||||
useFocusEffect(() => {
|
||||
focusEffect();
|
||||
|
||||
return focusEffectCleanup;
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const navigation = React.createRef<any>();
|
||||
|
||||
const element = (
|
||||
<NavigationContainer ref={navigation}>
|
||||
<TestNavigator>
|
||||
<Screen name="first" component={Test} />
|
||||
<Screen name="second">{() => null}</Screen>
|
||||
<Screen name="third">{() => null}</Screen>
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
render(element);
|
||||
|
||||
expect(focusEffect).toBeCalledTimes(1);
|
||||
expect(focusEffectCleanup).not.toBeCalled();
|
||||
|
||||
act(() => navigation.current.navigate('second'));
|
||||
|
||||
expect(focusEffect).toBeCalledTimes(1);
|
||||
expect(focusEffectCleanup).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('runs cleanup when component is unmounted', () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return descriptors[state.routes[state.index].key].render();
|
||||
};
|
||||
|
||||
const focusEffect = jest.fn();
|
||||
const focusEffectCleanup = jest.fn();
|
||||
|
||||
const TestA = () => {
|
||||
useFocusEffect(() => {
|
||||
focusEffect();
|
||||
|
||||
return focusEffectCleanup;
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const TestB = () => null;
|
||||
|
||||
const App = ({ mounted }: { mounted: boolean }) => (
|
||||
<NavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="first" component={mounted ? TestA : TestB} />
|
||||
<Screen name="second">{() => null}</Screen>
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
const root = render(<App mounted />);
|
||||
|
||||
expect(focusEffect).toBeCalledTimes(1);
|
||||
expect(focusEffectCleanup).not.toBeCalled();
|
||||
|
||||
root.update(<App mounted={false} />);
|
||||
|
||||
expect(focusEffect).toBeCalledTimes(1);
|
||||
expect(focusEffectCleanup).toBeCalledTimes(1);
|
||||
});
|
||||
@@ -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 useFocusEffect } from './useFocusEffect';
|
||||
|
||||
export * from './types';
|
||||
|
||||
43
src/useFocusEffect.tsx
Normal file
43
src/useFocusEffect.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import * as React from 'react';
|
||||
import useNavigation from './useNavigation';
|
||||
|
||||
type EffectCallback = (() => undefined) | (() => () => void);
|
||||
|
||||
export default function useFocusEffect(callback: EffectCallback) {
|
||||
const navigation = useNavigation();
|
||||
|
||||
React.useEffect(() => {
|
||||
let isFocused = false;
|
||||
let cleanup: (() => void) | undefined;
|
||||
|
||||
// We need to run the effect on intial render/dep changes if the screen is focused
|
||||
if (navigation.isFocused()) {
|
||||
cleanup = callback();
|
||||
isFocused = true;
|
||||
}
|
||||
|
||||
const unsubscribeFocus = navigation.addListener('focus', () => {
|
||||
// If callback was already called for focus, avoid calling it again
|
||||
// The focus event may also fire on intial render, so we guard against runing the effect twice
|
||||
if (isFocused) {
|
||||
return;
|
||||
}
|
||||
|
||||
cleanup && cleanup();
|
||||
cleanup = callback();
|
||||
isFocused = true;
|
||||
});
|
||||
|
||||
const unsubscribeBlur = navigation.addListener('blur', () => {
|
||||
cleanup && cleanup();
|
||||
cleanup = undefined;
|
||||
isFocused = false;
|
||||
});
|
||||
|
||||
return () => {
|
||||
cleanup && cleanup();
|
||||
unsubscribeFocus();
|
||||
unsubscribeBlur();
|
||||
};
|
||||
}, [callback, navigation]);
|
||||
}
|
||||
Reference in New Issue
Block a user