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(() => { console.log('Stale count in setInterval:', count); setCount(count + 1); }, 1000);
return () => clearInterval(intervalId); }, []); 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(() => { 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
执行时机不同
useLayoutEffect 的入参函数会在 react 更新 DOM 树后同步调用(其实就是dom树更新,浏览器重绘前会执行useLayoutEffect里的函数,此时比如去拿 style 或者 dom 尺寸,都是游览器即将渲染的那一次的尺寸,而不是现在页面上展示的尺寸),useEffect 为异步调用(useEffect 则肯定是在游览器渲染完后才执行),所以应该尽可能使用标准的 useEffect 以避免阻塞视觉更新
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); currentCount.current = count; useEffect(() => { const id = setInterval(() => { console.log(currentCount.current) setCount(currentCount.current + 1); }, 1000); return () => clearInterval(id); }, []); return <h1>{count}</h1>; }
|
useRef、useState如何决策用哪种来维护状态
useRef 生成的可变对象,因为使用起来就跟普通对象一样,赋值时候 React 是无法感知到值变更的,所以也不会触发组件重绘。利用其与 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.memo 或 shouldComponentUpdate,也会在组件本身使用 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 主要有两个作用:
- 缓存一些耗时计算,通过声明计算结果的依赖是否变更,来重用上次计算结果
- 保证引用不变,针对下游使用 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) => { }, [...deps]);
|
1 2 3
| const memoCallback = useMemo(() => (...args) => { }, [...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 }
const Child = forwardRef<ChildRef>((_, ref) => {
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 => { const subscribe = (callback: () => void) => { window.addEventListener('online', callback); window.addEventListener('offline', callback); return () => { window.removeEventListener('online', callback); window.removeEventListener('offline', callback); }; };
const getSnapshot = () => { return navigator.onLine; };
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) => { 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)); window.dispatchEvent(new StorageEvent('storage')); };
return [res, updateStorage]; };
|
useTransition(React 18)
useTransition 是一个帮助你在不阻塞UI的情况下更新状态的React Hook
用法:
1
| const [isPending, startTransition] = useTransition()
|
useTransition 返回一个数组,包含两个元素
- isPending(boolean),告诉你是否存在待处理的 transition。
- startTransition(function) 函数,你可以使用此方法将状态更新标记为 transition。startTransition里的更新必须是同步的
1 2 3 4 5 6 7 8 9 10 11 12 13
| startTransition(() => { setTimeout(() => { setPage('/about'); }, 1000); });
setTimeout(() => { startTransition(() => { setPage('/about'); }); }, 1000);
|
useTransition 的核心原理是将一部分状态更新处理为低优先级任务,这样可以将关键的高优先级任务先执行,而低优先级的过渡更新则会稍微延迟处理。这在渲染大量数据、进行复杂运算或处理长时间任务时特别有效。React 通过调度机制来管理优先级:
- 高优先级更新:直接影响用户体验的任务,比如表单输入、按钮点击等。
- 低优先级更新:相对不影响交互的过渡性任务,比如大量数据渲染、动画等,这些任务可以延迟执行。
示例:
长列表渲染时不阻塞用户输入
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)
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) => { 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) { } }); }
return ( <> {optimisticComments.map((c) => ( <div key={c.id} className={c.pending ? 'opacity-50' : ''}> {c.text} </div> ))} <form action={handleSubmit}>...</form> </> ); }
|
- 用户发起异步操作(如提交评论);
- 调用 addOptimistic(newComment)(或类似值)→ UI 立刻显示新评论;
- 在 startTransition 里执行异步请求;
- 请求成功:
- 真实状态更新;
- optimisticState 与真实状态一致,无需额外处理;
- 请求失败:
- 真实状态不变;
- useOptimistic 会把 optimisticState 回滚到真实状态,UI 自动恢复。
use(React 19)
use 是 React 19 提供的 Hook,主要做两件事:
- 读取 Promise:在 Promise 未完成时挂起组件,完成后返回结果;
- 读取 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
| function fetchUser(id) { return fetch(`/api/users/${id}`).then((r) => r.json()); }
function UserProfile({ userId }) { const user = use(fetchUser(userId)); 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