Hooks

useEffect

执行副作用,一般情况下会在浏览器绘制页面后异步调用

useEffect() 的用途

只要是副效应,都可以使用useEffect()引入。它的常见用途有下面几种。

  • 获取数据(data fetching)
  • 事件监听或订阅(setting up a subscription)
  • 改变 DOM(changing the DOM)
  • 输出日志(logging)

useEffect返回值

副效应是随着组件加载而发生的,那么组件卸载时,可能需要清理这些副效应。

useEffect()允许返回一个函数,在组件卸载时,执行该函数,清理副效应。如果不需要清理副效应,useEffect()就不用返回任何值。

1
2
3
4
5
6
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
}, [props.source]);

上面例子中,useEffect()在组件加载时订阅了一个事件,并且返回一个清理函数,在组件卸载时取消订阅。

实际使用中,由于副效应函数默认是每次渲染都会执行,所以清理函数不仅会在组件卸载时执行一次,每次副效应函数重新执行之前,也会执行一次,用来清理上一次渲染的副效应。

useEffect 中如何使用 async/await

1)async 函数抽离到外部

1
2
3
4
5
6
7
8
9
async function fetchMyAPI() {
let response = await fetch("api/data");
response = await res.json();
ådataSet(response);
}

useEffect(() => {
fetchMyAPI();
}, []);

2)async 立即执行函数

1
2
3
4
5
useEffect(() => { 
(async function anyNameFunction() {
await loadContent();
})();
}, []);

3)ahooks - useAsyncEffect

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
function useAsyncEffect(
effect: () => AsyncGenerator<void, void, void> | Promise<void>,
deps?: DependencyList,
) {
useEffect(() => {
const e = effect();
let cancelled = false;
async function execute() {
if (isAsyncGenerator(e)) {
while (true) {
const result = await e.next();
if (result.done || cancelled) {
break;
}
}
} else {
await e;
}
}
execute();
return () => {
cancelled = true;
};
}, deps);
}

useEffect 闭包陷阱

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
import React, { useState, useEffect } from 'react';

function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
// 模拟一个异步操作,例如网络请求
const intervalId = setInterval(() => {
// 这里的 count 捕获的是 useEffect 第一次运行时 count 的值 (0)
// 因此,每次执行都会是 0 + 1 = 1,而不是递增
console.log('Stale count in setInterval:', count);
setCount(count + 1); // 这是一个闭包陷阱!
}, 1000);

return () => clearInterval(intervalId);
}, []); // 依赖项为空数组,表示只在组件挂载时执行一次
// 导致 setInterval 内部的 count 始终是初始值 0

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

export default Counter;

useLayoutEffect

与 useEffect 类似,但在浏览器完成绘制之前同步执行
适用于需要读取 DOM 布局或强制同步更新的场景(如测量DOM、同步修改DOM避免闪烁)。

代码示例:

记录滚动条的位置

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
function App() {
const srollHandler = (e: React.UIEvent<HTMLDivElement>) => {
const scrolltop = e.currentTarget.scrollTop;
window.history.replaceState(null, '', `?top=${scrolltop}`);
};

useLayoutEffect(() => {
// 若使用useEffect,定位到滚动条位置前会先渲染出最初始的状态,会有明显的闪烁
// useEffect(() => {
const top = window.location.search.split('=')[1];
if (top) {
const container = document.getElementById('container');
container?.scrollTo(0, Number(top));
}
}, []);

return (
<div onScroll={srollHandler} id="container" style={{ height: '400px', overflowY: 'auto' }}>
{Array(1000)
.fill(0)
.map((item, index) => {
return <div key={index}>{index}</div>;
})}
</div>
);
}

export default App;

useLayoutEffect vs useEffect

  1. 执行时机不同
    useLayoutEffect 的入参函数会在 react 更新 DOM 树后同步调用(其实就是dom树更新,浏览器重绘前会执行useLayoutEffect里的函数,此时比如去拿 style 或者 dom 尺寸,都是游览器即将渲染的那一次的尺寸,而不是现在页面上展示的尺寸),useEffect 为异步调用(useEffect 则肯定是在游览器渲染完后才执行),所以应该尽可能使用标准的 useEffect 以避免阻塞视觉更新

  2. useLayoutEffect 在 development 模式下 SSR 会有警告⚠️

通常情况下 useLayoutEffect 会用在做动效和记录 layout 的一些特殊场景(比如防止渲染闪烁,在渲染前再给你个机会去改 DOM)。一般不需要使用 useLayoutEffect。

useRef

当需要存放一个数据,需要无论在哪里都取到最新状态时,需要使用 useRef,ref 是一种可变数据。
如上文提到的代码段中,因为存在闭包问题,count永远会为1,log永远为0;
可以引入useRef解决这个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Counter() {
const [count, setCount] = useState(0);
const currentCount = useRef(count);
// 这里在每一次组件重新渲染后更新ref的值
currentCount.current = count;
useEffect(() => {
const id = setInterval(() => {
console.log(currentCount.current)
setCount(currentCount.current + 1); // 这个 effect 依赖于 `count` state
}, 1000);
return () => clearInterval(id);
}, []); // 🔴 Bug: `count` 没有被指定为依赖
return <h1>{count}</h1>;
}

useRef、useState如何决策用哪种来维护状态

useRef 生成的可变对象,因为使用起来就跟普通对象一样,赋值时候 React 是无法感知到值变更的,所以也不会触发组件重绘。利用其与 useState 的区别,我们一般这样区分使用:

  • 维护与 UI 相关的状态,使用 useState

确保更改时刷新 UI

  • 值更新不需要触发重绘时,使用 useRef
  • 不需要变更的数据、函数,使用 useState

比如,需要声明一个不可变的值时,可以这样:

1
const [immutable] = useState(someState);

不返回变更入口函数。useRef 虽然可以借助 TypeScript 达到语法检测上的 immutable,但实际还是 mutable 的。

useContext

获取 context 对象,用于在组件树中获取和使用共享的上下文。

context简介:

React.createContext 新建context

1
const MyContext = React.createContext(defaultValue);

创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。

只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。此默认值有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。

Context.Provider

每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。

useContext使用

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>value prop 决定。

当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memoshouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。

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
34
35
36
37
38
39
40
41
42
import React, { useContext, useState } from "react";
import { ThemeContext, themes } from "./utils/index";

export default function App() {
const [theme, setTheme] = useState(themes.light);
return (
<ThemeContext.Provider
value={{
theme,
toggle: () =>
setTheme(
theme === themes.light ? themes.dark : themes.light
),
}}
>
<Toolbar />
</ThemeContext.Provider>
);
}

function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}

function ThemedButton() {
const context = useContext(ThemeContext);
return (
<button
style={{
background: context.theme.background,
color: context.theme.foreground,
}}
onClick={context.toggle}
>
I am styled by theme context!
</button>
);
}

useMemo

useMemo 主要有两个作用:

  1. 缓存一些耗时计算,通过声明计算结果的依赖是否变更,来重用上次计算结果
  2. 保证引用不变,针对下游使用 React.memo 的组件进行性能优化(useCallback 也有一样的作用)

比如,计算耗时的 fibonacci 数列,就可以用 useMemo 来优化在 n 不变的情况下,二次渲染的性能

useMemo(() => { return fibonacci(props.n) }, [props.n]);

结合React.memo防止组件re-render

多次点击后输出结果

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
34
35
36
37
import React, { memo, useMemo, useState } from "react";

const Heading = memo(({ style, title }) => {
console.log("Rendered:", title);
return <h1 style={style}>{title}</h1>;
});

export default function App() {
const [count, setCount] = useState(0);

const normalStyle = {
backgroundColor: "teal",
color: "white",
};

const memoizedStyle = useMemo(() => {
return {
backgroundColor: "red",
color: "white",
};
}, []);

return (
<>
<button
onClick={() => {
setCount(count + 1);
}}
>
Increment {count}
</button>
<Heading style={memoizedStyle} title="Memoized" />
<Heading style={normalStyle} title="Normal" />
<Heading title="React.memo Normal" />
</>
);
}

点击按钮后只会输出:Rendered: Normal

React 组件是一个树形结构,且每个节点都是懒计算的(类似于 Thunk 的概念)。当一个节点不需要重新计算(重绘)时,他的子树都不会计算(重绘)。所以我们做性能优化的目标,就是在尽量离根节点近的位置,拦截不必要的节点重算,从而减少重绘的计算量。

useCallback

useCallback 是简化版的 useMemo,方便缓存函数引用

下面的代码是等价的:

1
2
3
const memoCallback = useCallback((...args) => {
// DO SOMETHING
}, [...deps]);
1
2
3
const memoCallback = useMemo(() => (...args) => {
// DO SOMETHING
}, [...deps]);

在没有遇到性能问题时,不要使用 useCallback 和 useMemo,性能优化先交给框架处理解决。手工的微优化在没有对框架和业务场景有深入了解时,可能出现性能劣化。

致命的 useCallback/useMemo(翻译)

useCallback hell问题总结

关于如何减少 useCallback 看 第二天

useReducer

用于管理复杂状态逻辑的替代方案,类似于 Redux 的 reducer。

useImperativeHandle

可以在子组件内部暴露给父组件句柄,类似于Vue的defineExpose

React18版本需要配合forwardRef一起使用,React19版本不需要配合forwardRef一起使用,直接使用即可

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
interface ChildRef {
name: string
count: number
addCount: () => void
subCount: () => void
}

// React 18
const Child = forwardRef<ChildRef>((_, ref) => {
// React19
// const Child = ({ ref }: { ref: React.Ref<ChildRef> }) => {
const [count, setCount] = useState(0)
//重点
useImperativeHandle(ref, () => {
return {
name: 'child',
count,
addCount: () => setCount(count + 1),
subCount: () => setCount(count - 1)
}
})
return <div>
<h3>我是子组件</h3>
<div>count:{count}</div>
<button onClick={() => setCount(count + 1)}>增加</button>
<button onClick={() => setCount(count - 1)}>减少</button>
</div>
})

function App() {
const childRef = useRef<ChildRef>(null)
const showRefInfo = () => {
console.log(childRef.current)
}
return (
<div>
<h2>我是父组件</h2>
<button onClick={showRefInfo}>获取子组件信息</button>
<button onClick={() => childRef.current?.addCount()}>操作子组件+1</button>
<button onClick={() => childRef.current?.subCount()}>操作子组件-1</button>
<hr />
<Child ref={childRef}></Child>
</div>
);
}

export default App;

场景示例:
封装一个表单组件,提供了两个方法:校验和重置。使用useImperativeHandle可以将这些方法暴露给父组件,父组件便可以通过ref调用子组件的方法。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
interface ChildRef {
name: string
validate: () => string | true
reset: () => void
}

const Child = ({ ref }: { ref: React.Ref<ChildRef> }) => {
const [form, setForm] = useState({
username: '',
password: '',
email: ''
})
const validate = () => {
if (!form.username) {
return '用户名不能为空'
}
if (!form.password) {
return '密码不能为空'
}
if (!form.email) {
return '邮箱不能为空'
}
return true
}
const reset = () => {
setForm({
username: '',
password: '',
email: ''
})
}
useImperativeHandle(ref, () => {
return {
name: 'child',
validate: validate,
reset: reset
}
})
return <div style={{ marginTop: '20px' }}>
<h3>我是表单组件</h3>
<input value={form.username} onChange={(e) => setForm({ ...form, username: e.target.value })} placeholder='请输入用户名' type="text" />
<input value={form.password} onChange={(e) => setForm({ ...form, password: e.target.value })} placeholder='请输入密码' type="text" />
<input value={form.email} onChange={(e) => setForm({ ...form, email: e.target.value })} placeholder='请输入邮箱' type="text" />
</div>
}

function App() {
const childRef = useRef<ChildRef>(null)
const showRefInfo = () => {
console.log(childRef.current)
}
const submit = () => {
const res = childRef.current?.validate()
console.log(res)
}
return (
<div>
<h2>我是父组件</h2>
<button onClick={showRefInfo}>获取子组件信息</button>
<button onClick={() => submit()}>校验子组件</button>
<button onClick={() => childRef.current?.reset()}>重置</button>
<hr />
<Child ref={childRef}></Child>
</div>
);
}

export default App;

useDebugValue

用于在开发者工具中显示自定义的钩子相关标签。

useSyncExternalStore(React 18)

useSyncExternalStore 是一个订阅外部 store 的 React Hook

场景

  • 订阅外部store(Redux、Mobx、Zustand)
  • 订阅浏览器Api(online、storage、location、history hash)
  • 抽离逻辑、编写自定义hooks
  • 服务端渲染

用法

1
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const useOnline = (): boolean => {
// 订阅外部数据源(浏览器的 online/offline 事件)
const subscribe = (callback: () => void) => {
// subcribe外部数据源变化时调用 callback()
// React 收到通知后调用 getSnapshot() 获取最新值
// 值变化时触发组件更新
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
// 取消订阅
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
};

// 获取当前快照值
const getSnapshot = () => {
return navigator.onLine;
};

// 使用 useSyncExternalStore 订阅外部状态
return useSyncExternalStore(subscribe, getSnapshot);
};
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
import { useSyncExternalStore } from 'react';
export const useStorage = (key: string, initValue: any) => {
// 订阅者
const subscribe = (callback: () => void) => {
// 订阅浏览器的api
window.addEventListener('storage', callback);
return () => {
// 取消订阅
window.removeEventListener('storage', callback);
};
};
const getSnapshot = () => {
return localStorage.getItem(key) ? JSON.parse(localStorage.getItem(key)!) : initValue;
};
const res = useSyncExternalStore(subscribe, getSnapshot);

const updateStorage = (value: any) => {
localStorage.setItem(key, JSON.stringify(value));
// 通知订阅者 手动触发storage事件
window.dispatchEvent(new StorageEvent('storage'));
};

return [res, updateStorage];
};
// 使用
// const [count, setCount] = useStorage("count", 0)

useTransition(React 18)

useTransition 是一个帮助你在不阻塞UI的情况下更新状态的React Hook

用法:

1
const [isPending, startTransition] = useTransition()

useTransition 返回一个数组,包含两个元素

  1. isPending(boolean),告诉你是否存在待处理的 transition。
  2. startTransition(function) 函数,你可以使用此方法将状态更新标记为 transition。startTransition里的更新必须是同步的
1
2
3
4
5
6
7
8
9
10
11
12
13
startTransition(() => {
// ❌ 在调用 startTransition 后更新状态
setTimeout(() => {
setPage('/about');
}, 1000);
});

setTimeout(() => {
startTransition(() => {
// ✅ 在调用 startTransition 中更新状态
setPage('/about');
});
}, 1000);

useTransition 的核心原理是将一部分状态更新处理为低优先级任务,这样可以将关键的高优先级任务先执行,而低优先级的过渡更新则会稍微延迟处理。这在渲染大量数据、进行复杂运算或处理长时间任务时特别有效。React 通过调度机制来管理优先级:

  1. 高优先级更新:直接影响用户体验的任务,比如表单输入、按钮点击等。
  2. 低优先级更新:相对不影响交互的过渡性任务,比如大量数据渲染、动画等,这些任务可以延迟执行。

示例:

长列表渲染时不阻塞用户输入

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
import React, { useLayoutEffect, useState, useReducer, useTransition } from 'react';
import { Input, List } from 'antd';
interface Iitem {
id: string;
name: string;
address: string;
age: number;
}
function App() {
const [inputValue, setInputValue] = useState('');
const [list, setList] = useState<Iitem[]>([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
setInputValue(val);
fetch('/api/mock/list?key=' + val)
.then((res) => res.json())
.then((res) => {
startTransition(() => {
setList(res.list);
});
});
};
return (
<>
<Input value={inputValue} onChange={handleChange} />
{isPending && <div>loading...</div>}
<List dataSource={list} renderItem={(item) => <List.Item>{item.address}</List.Item>} />
</>
);
}

export default App;

useDeferredValue(React 18)

useDeferredValue 可以让你延迟更新 UI 的某些部分。

其实有点类似防抖,只不过防抖需要设置一个具体时间,而useDeferredValue延迟多久是由设备性能决定的,还有一个关键是:useDeferredValue 执行的延迟重新渲染默认是可中断的。React官方也提供了说明:延迟一个值与防抖和节流之间有什么不同?

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { useState, useDeferredValue } from 'react';
import SlowList from './SlowList.js';

export default function App() {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<SlowList text={deferredText} />
</>
);
}

useTransition 和 useDeferredValue 的区别

useTransition 和 useDeferredValue 都是 React 18 并发渲染的核心 Hook,用于区分紧急 / 非紧急更新,解决大数据渲染阻塞用户交互的问题,提升页面响应性;

  • useTransition是主动标记非紧急的setState。它允许开发者控制某个更新的延迟更新,还提供了过渡标识(isPendding),让开发者能够添加过渡反馈
  • useDeferredValue是被动创建值的延迟副本。一般还需要配合React.memo使用

useId(React 18)

useActionState(React 19)

useFormStatus(React 19)

useOptimistic(React 19)

useOptimistic 是 React 19 提供的 Hook,用来做乐观更新:在异步操作还没完成时,先假设成功并更新 UI;如果最终失败,再回滚。

典型场景:表单提交、评论、点赞等:

  • 用户点击提交 → 立刻在 UI 里显示结果;
  • 后台请求还在进行;
  • 成功:保持当前 UI;
  • 失败:撤销刚才的 UI 变化,并提示错误。

API形式:

1
2
3
4
5
6
7
const [optimisticState, addOptimistic] = useOptimistic(
state,
(currentState, optimisticValue) => {
// 根据 optimisticValue 算出“乐观 UI”对应的状态
return optimisticState;
}
);
  • 第一个参数:state:真实数据(来自服务端或 props);
  • 第二个参数:更新函数,接收 (currentState, optimisticValue),返回新的乐观状态;
  • 返回值:[optimisticState, addOptimistic];
    • optimisticState:当前展示用的状态(可能是真实状态,也可能是乐观状态);
    • addOptimistic(optimisticValue):触发一次乐观更新。

示例:

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
function CommentList({ comments, addComment }) {
const [optimisticComments, addOptimisticComment] = useOptimistic(
comments,
(state, newComment) => [...state, { ...newComment, pending: true }]
);

async function handleSubmit(formData) {
const optimistic = { id: 'temp', text: formData.get('text'), pending: true };
addOptimisticComment(optimistic);

startTransition(async () => {
try {
await addComment(formData);
} catch (e) {
// 失败时 useOptimistic 会自动回滚
}
});
}

return (
<>
{optimisticComments.map((c) => (
<div key={c.id} className={c.pending ? 'opacity-50' : ''}>
{c.text}
</div>
))}
<form action={handleSubmit}>...</form>
</>
);
}
  1. 用户发起异步操作(如提交评论);
  2. 调用 addOptimistic(newComment)(或类似值)→ UI 立刻显示新评论;
  3. 在 startTransition 里执行异步请求;
  4. 请求成功:
    • 真实状态更新;
    • optimisticState 与真实状态一致,无需额外处理;
  5. 请求失败:
    • 真实状态不变;
    • useOptimistic 会把 optimisticState 回滚到真实状态,UI 自动恢复。

use(React 19)

use 是 React 19 提供的 Hook,主要做两件事:

  1. 读取 Promise:在 Promise 未完成时挂起组件,完成后返回结果;
  2. 读取 Context:类似 useContext,但可以在条件、循环里调用。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 封装一个 fetch
function fetchUser(id) {
return fetch(`/api/users/${id}`).then((r) => r.json());
}

function UserProfile({ userId }) {
const user = use(fetchUser(userId)); // 等待 Promise,期间 suspend
return (
<div>
<h1>{user.name}</h1>
<p>{user.bio}</p>
</div>
);
}

// 页面里
function Page() {
return (
<Suspense fallback={<div>加载中...</div>}>
<UserProfile userId={123} />
</Suspense>
);
}

相对于useEffect + loading的方案,use + Suspense这种用法把loading提到父级,更适合流式SSR