Skip to main content
Version: 6.x

useFocusEffect

有时我们想在屏幕聚焦时运行副作用。副作用可能涉及添加事件监听器、获取数据、更新文档标题等。虽然这可以使用 focusblur 事件来实现,但它不太符合人机工程学。

¥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.

为了使这更容易,该库导出了 useFocusEffect 钩子:

¥To make this easier, the library exports a useFocusEffect hook:

import { useFocusEffect } from '@react-navigation/native';

function Profile({ userId }) {
const [user, setUser] = React.useState(null);

useFocusEffect(
React.useCallback(() => {
const unsubscribe = API.subscribe(userId, (user) => setUser(user));

return () => unsubscribe();
}, [userId])
);

return <ProfileContent user={user} />;
}
警告

为了避免过于频繁地运行效果,重要的是在将回调传递给 useFocusEffect 之前将回调封装在 useCallback 中,如示例所示。

¥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.

useFocusEffect 类似于 React 的 useEffect 钩子。唯一的区别是它仅在屏幕当前聚焦时运行。

¥The useFocusEffect is analogous to React's useEffect hook. The only difference is that it only runs if the screen is currently focused.

只要传递给 React.useCallback 的依赖发生更改,该效果就会运行,即,它将在初始渲染(如果屏幕聚焦)上运行,如果依赖发生更改,它将在后续渲染上运行。如果你没有将效果封装在 React.useCallback 中,则当屏幕聚焦时,该效果将在每次渲染时运行。

¥The effect will run whenever the dependencies passed to React.useCallback change, i.e. it'll run on initial render (if the screen is focused) as well as on subsequent renders if the dependencies have changed. If you don't wrap your effect in React.useCallback, the effect will run every render if the screen is focused.

当需要清理先前的效果时,即当依赖发生变化并计划新的效果时以及当屏幕卸载或模糊时,清理函数就会运行。

¥The cleanup function runs when the previous effect needs to be cleaned up, i.e. when dependencies change and a new effect is scheduled and when the screen unmounts or blurs.

运行异步效果

¥Running asynchronous effects

当运行异步效果(例如从服务器获取数据)时,务必确保在清理函数中取消请求(类似于 React.useEffect)。如果你使用的 API 不提供取消机制,请确保忽略状态更新:

¥When running asynchronous effects such as fetching data from server, it's important to make sure that you cancel the request in the cleanup function (similar to React.useEffect). If you're using an API that doesn't provide a cancellation mechanism, make sure to ignore the state updates:

useFocusEffect(
React.useCallback(() => {
let isActive = true;

const fetchUser = async () => {
try {
const user = await API.fetch({ userId });

if (isActive) {
setUser(user);
}
} catch (e) {
// Handle error
}
};

fetchUser();

return () => {
isActive = false;
};
}, [userId])
);

如果你不忽略结果,则可能会由于 API 调用中的竞争条件而导致数据不一致。

¥If you don't ignore the result, then you might end up with inconsistent data due to race conditions in your API calls.

延迟效果直到过渡完成

¥Delaying effect until transition finishes

一旦屏幕成为焦点,useFocusEffect 钩子就会运行效果。这通常意味着,如果有屏幕变化的动画,它可能还没有完成。

¥The useFocusEffect hook runs the effect as soon as the screen comes into focus. This often means that if there is an animation for the screen change, it might not have finished yet.

React Navigation 在原生线程中运行其动画,因此在许多情况下这不是问题。但如果该效果更新了 UI 或渲染了一些昂贵的内容,那么它可能会影响动画性能。在这种情况下,我们可以使用 InteractionManager 推迟我们的工作,直到动画或手势完成:

¥React Navigation runs its animations in native thread, so it's not a problem in many cases. But if the effect updates the UI or renders something expensive, then it can affect the animation performance. In such cases, we can use InteractionManager to defer our work until the animations or gestures have finished:

useFocusEffect(
React.useCallback(() => {
const task = InteractionManager.runAfterInteractions(() => {
// Expensive task
});

return () => task.cancel();
}, [])
);

useFocusEffect 与为 focus 事件添加监听器有何不同

¥How is useFocusEffect different from adding a listener for focus event

当屏幕成为焦点时,会触发 focus 事件。由于它是一个事件,因此如果你在订阅该事件时屏幕已经聚焦,则不会调用你的监听器。这也没有提供在屏幕失去焦点时执行清理功能的方法。你可以订阅 blur 事件并手动处理它,但它可能会变得混乱。除了这些事件之外,你通常还需要处理 componentDidMountcomponentWillUnmount,这使事情变得更加复杂。

¥The focus event fires when a screen comes into focus. Since it's an event, your listener won't be called if the screen was already focused when you subscribed to the event. This also doesn't provide a way to perform a cleanup function when the screen becomes unfocused. You can subscribe to the blur event and handle it manually, but it can get messy. You will usually need to handle componentDidMount and componentWillUnmount as well in addition to these events, which complicates it even more.

useFocusEffect 允许你在焦点上运行效果,并在屏幕失去焦点时清理它。它还处理卸载时的清理。当依赖发生变化时,它会重新运行效果,因此你无需担心监听器中的陈旧值。

¥The useFocusEffect allows you to run an effect on focus and clean it up when the screen becomes unfocused. It also handles cleanup on unmount. It re-runs the effect when dependencies change, so you don't need to worry about stale values in your listener.

何时使用 focusblur 事件

¥When to use focus and blur events instead

useEffect 一样,可以从 useFocusEffect 中的效果返回清理函数。cleanup 函数的目的是清理效果 - 例如中止异步任务、取消订阅事件监听器等。它不适合用于在 blur 上执行某些操作。

¥Like useEffect, a cleanup function can be returned from the effect in useFocusEffect. The cleanup function is intended to cleanup the effect - e.g. abort an asynchronous task, unsubscribe from an event listener, etc. It's not intended to be used to do something on blur.

例如,请勿执行以下操作:

¥For example, don't do the following:

useFocusEffect(
React.useCallback(() => {
return () => {
// Do something that should run on blur
};
}, [])
);

只要效果需要清理,即在 blur 上、卸载、依赖更改等,清理函数就会运行。这不是更新状态或执行应在 blur 上发生的操作的好地方。你应该使用监听 blur 事件来代替:

¥The cleanup function runs whenever the effect needs to cleanup, i.e. on blur, unmount, dependency change etc. It's not a good place to update the state or do something that should happen on blur. You should use listen to the blur event instead:

React.useEffect(() => {
const unsubscribe = navigation.addListener('blur', () => {
// Do something when the screen blurs
});

return unsubscribe;
}, [navigation]);

同样,如果你想在屏幕接收焦点时执行某些操作(例如跟踪屏幕焦点)并且不需要清理或需要在依赖更改时重新运行,那么你应该使用 focus 事件:

¥Similarly, if you want to do something when the screen receives focus (e.g. track screen focus) and it doesn't need cleanup or need to be re-run on dependency changes, then you should use the focus event instead:

与类组件一起使用

¥Using with class component

你可以为你的效果创建一个组件并在你的类组件中使用它:

¥You can make a component for your effect and use it in your class component:

function FetchUserData({ userId, onUpdate }) {
useFocusEffect(
React.useCallback(() => {
const unsubscribe = API.subscribe(userId, onUpdate);

return () => unsubscribe();
}, [userId, onUpdate])
);

return null;
}

// ...

class Profile extends React.Component {
_handleUpdate = (user) => {
// Do something with user object
};

render() {
return (
<>
<FetchUserData
userId={this.props.userId}
onUpdate={this._handleUpdate}
/>
{/* rest of your code */}
</>
);
}
}