React|useEffect

声明Effect

来看一个简单的案例,我们希望实现一个播放视频的组件,通过页面上的按钮点击来控制播放暂停

video的DOM元素本身并没有这样的功能,只提供了pause和play的函数

因此我们会进行如下的初步的设计

1
2
3
4
5
6
7
8
9
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
return <video ref={ref} src={src} loop playsInline />;
}

上述的组件中使用到了useRef钩子,这里简单认为我们通过它来获取DOM元素的引用,对video DOM进行操作

但是上述的实现并不正确

  • 首先在组件的元素渲染逻辑部分,不应当直接操作DOM
  • 其次在初次渲染的时候,ref因为还没有走到生成<video>的部分,因此保留了null的默认值。React 在返回 JSX 之前不知道要创建什么样的 DOM,所以没有 DOM 节点可以调用 play()pause() 方法

这个时候我们可以使用useEffect钩子

1
2
3
4
5
6
7
8
9
10
11
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
});
return <video ref={ref} src={src} loop playsInline />;
}

此时组件的渲染顺序为:

  • React 会更新页面,确保 <video> 标签带着正确的 props 出现在 DOM 中
  • React 将运行 Effect,Effect 将根据 isPlaying 的值调用 play()pause()

Effect的触发时机

默认情况下,Effect会在组件被渲染后运行

而我们如果更新组件的state,会触发组件再次进行渲染

因此下列的代码是死循环

1
2
3
4
5
6
const [count, setCount] = useState(0);
//组件每次渲染结束后执行
useEffect(() => {
//更新state,再次触发组件渲染
setCount(count + 1);
});

设置Effect依赖项

上面提到了,上述声明的Effect默认是在每一次组件渲染后都会被执行

但是有的时候我们Effect并不希望在每一次渲染后就执行:

  • 服务器连接,不能多建立,只希望组件第一次渲染的时候才建立
  • 组件中其他修改state的行为不应该重新导致Effect执行

来看下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);

useEffect(() => {
if (isPlaying) {
console.log('调用 video.play()');
ref.current.play();
} else {
console.log('调用 video.pause()');
ref.current.pause();
}
});

return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
const [isPlaying, setIsPlaying] = useState(false);
const [text, setText] = useState('');
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<button onClick={() => setIsPlaying(!isPlaying)}>
{isPlaying ? '暂停' : '播放'}
</button>
<VideoPlayer
isPlaying={isPlaying}
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
/>
</>
);
}

我们多加了一个 const [text, setText] = useState('');

这就导致我们每次修改文本框的内容,都会触发一次setText,导致state更新,导致组件重新渲染,最终导致Effect被运行

这时候我们可以通过指定Effect的依赖项来解决这一问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//只要组件渲染就都会调用
useEffect(() => {
//do something
});

//只有组件第一次渲染(也叫挂载)的时候才会调用
useEffect(() => {
//do something
}, []);

//只要a或者b发生了变化的时候就会调用
useEffect(() => {
//do something
}, [a,b]);

cleanup函数

上面我们提到了,建立连接的场景可能也会需要为Effect设置依赖项。假如我们正在编写一个 ChatRoom 组件,该组件在显示时需要连接到聊天服务器。现在为你提供了 createConnection() API,该 API 返回一个包含 connect()disconnection() 方法的对象。如何确保组件在显示时始终保持连接?

1
2
3
4
useEffect(()=>{
const connection = createConnection();
connection.connect();
}, [])

这样虽然实现了只会在组件第一次渲染也就是挂载的时候建立连接,但是存在关键问题,如果切换页面,组件再次第一次渲染的时候,就会创建第二个连接

我们需要在 useEffect 中实现类似 finally 块的效果,在组件卸载(被移除)时最后一次调用,或者在每次 Effect 重新运行的时候调用

这个时候我们就需要使用 useEffect 提供的 cleanUp 函数,写法就是进行一个 return

1
2
3
4
5
6
useEffect(()=>{
const connection = createConnection();
connection.connect();
//cleanup
return () => connection.disconnection();
}, [])

值得注意的是,在开发模式下,默认React会针对所有的Effect在第一次渲染后再调用一次渲染,目的就是为了及时发现在Effect主函数体中申请了资源但是没有cleanUp的bug


React|useEffect
http://example.com/2025/03/04/React-useEffect/
作者
Noctis64
发布于
2025年3月4日
许可协议