useActionState
useActionState 是一个专门用来包裹“异步 Action”(比如表单提交、接口请求)的 Hook。它能自动帮你管理“请求中(isPending)”、“返回数据(data/state)”以及“表单 Action 触发器”这三个核心状态
- 参数说明
- fn (Action 函数):当表单提交时触发的异步或同步函数。它会接收两个参数: previousState:上一次的 State 状态。 formData(或传入的其他参数):如果是表单触发,它会自动收到标准的 FormData 对象
- initialState:初始状态(比如初始的错误信息或成功提示,可以设为 null )
- permalink(可选):主要用于服务端渲染(SSR)的渐进式增强
- 返回值
- formState:当前 Action 执行完后返回的结果(由你的 fn 函数 return 的值决定)。
- formAction:安全的 Action 触发器,直接传给 <\form action={formAction}>。
- isPending:一个布尔值,代表当前的异步 Action 是否正在执行(自带防抖/防重复提交效果)。
- 代码示例
js// 以前传统、繁琐的写法
const [error, setError] = useState(null);
const [isPending, setIsPending] = useState(false);
async function handleSubmit(e) {
e.preventDefault();
setIsPending(true);
try {
await signupAction(formData);
} catch (err) {
setError(err.message);
} finally {
setIsPending(false);
}
}
js// 最新写法(一目了然)
import { useActionState } from 'react';
// 1. 定义一个异步的 Action 函数
async function signUp(previousState, formData) {
const email = formData.get("email");
// 模拟 API 请求
try {
const res = await fetch('/api/signup', {
method: 'POST',
body: JSON.stringify({ email })
});
const data = await res.json();
if (!res.ok) {
return { success: false, message: data.error }; // 👈 return 的对象会成为新的 formState
}
return { success: true, message: "注册成功!" };
} catch (err) {
return { success: false, message: "网络错误,请稍后再试" };
}
}
export default function SignUpForm() {
// 2. 初始化 useActionState
const [state, formAction, isPending] = useActionState(signUp, { success: false, message: "" });
return (
<form action={formAction}> {/* 3. 将 formAction 直接绑定到 action 属性 */}
<input type="email" name="email" required placeholder="请输入邮箱" />
{/* 4. 利用 isPending 自动控制禁用状态,无需手动设置 */}
<button type="submit" disabled={isPending}>
{isPending ? '提交中...' : '注册'}
</button>
{/* 5. 渲染返回的错误或成功状态 */}
{state.message && (
<p style={{ color: state.success ? 'green' : 'red' }}>
{state.message}
</p>
)}
</form>
);
}
useDeferredValue
useDeferredValue 可以让你“延迟”更新某一部分不那么紧急的状态 它接受一个值,并返回这个值的副本。当新值到来时,React 会先用旧值快速渲染紧急的部分(如输入框),然后在后台“悄悄”处理新值的渲染。一旦后台计算完成,再把新结果替换上去
- 参数说明:
- value:你想延迟的值(可以是字符串、数组、对象等任何类型)
- 返回值:
- deferredValue:延迟后的新值。在 React 忙于其他紧急更新时,它会保持上一次的旧值;一旦空闲,它就会追上最新的 value。
- 代码示例
jsimport { useState, useDeferredValue, useMemo } from 'react';
// 假设这是一个渲染极其耗时的列表组件
function HugeList({ text }) {
// 模拟一个大数据的过滤计算
const items = useMemo(() => {
const list = [];
for (let i = 0; i < 10000; i++) {
if (i.toString().includes(text)) {
list.push(<li key={i}>结果项 #{i}</li>);
}
}
return list;
}, [text]);
return <ul>{items}</ul>;
}
export default function SearchPage() {
const [query, setQuery] = useState('');
// 💡 核心:把 query 包装成一个延迟生效的变量
const deferredQuery = useDeferredValue(query);
return (
<div>
{/* 输入框绑定的是最新、最实时的 query */}
<input
type="text"
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="搜索..."
/>
{/* 大列表绑定的是延迟后的 deferredQuery。
当用户打字时,query 瞬间改变,输入框立刻流畅响应;
而 HugeList 接收到的 deferredQuery 还在原地等一下,直到浏览器空闲才更新。
*/}
<HugeList text={deferredQuery} />
</div>
);
}
deferredQuery 是没那么快更新的。在用户输入的时候,query 会作为UI紧急任务渲染,但是 deferredQuery 会在浏览器的空闲时间才会更新。
useTransition
并发并发渲染(Concurrent Mode)后,诞生了一个极具人性化的性能优化 Hook:useTransition。 它能把原本会造成页面卡顿的重度渲染,变成可中断的后台渲染,从而让你的网页在执行大量计算时,依然能够对用户的输入、点击做到“秒响应”。
- 参数说明:
- startTransition:一个包装函数。包裹在它里面的状态更新(如 setList(...))会被降级为“低优先级”。React 会优先保证用户的打字、点击等紧急操作流畅运行,在后台偷偷渲染低优先级的任务。最厉害的是,如果用户再次打字,React 会毫不犹豫地“掐断”上一次没渲染完的低优先级任务,直接开始全新的渲染!
- isPending:一个布尔值。当后台正在静悄悄地渲染低优先级任务时,isPending 为 true。你可以用它来在页面上转个小圈圈(Loading),告诉用户“后台正忙着呢”。
- 代码示例:
jsimport { useState, useTransition } from 'react';
export default function FilterList() {
const [query, setQuery] = useState(''); // 输入框的值(高优先级)
const [list, setList] = useState([]); // 海量列表数据(低优先级)
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
// 1. 🚀 高优先级更新:立刻让输入框响应用户的打字
setQuery(value);
// 2. 🐢 低优先级更新:把耗时的筛选逻辑扔进 startTransition
startTransition(() => {
// 模拟生成 10000 条繁重的数据更新
const results = Array.from({ length: 10000 }, (_, i) => `${value} 结果 ${i}`);
setList(results);
});
};
return (
<div>
<input type="text" value={query} onChange={handleChange} />
{/* 3. 利用 isPending 给用户极佳的视觉反馈 */}
{isPending && <p style={{ color: 'blue' }}>正在努力加载新数据...</p>}
<div style={{ opacity: isPending ? 0.5 : 1 }}>
{list.map((item, index) => <div key={index}>{item}</div>)}
</div>
</div>
);
}
- 避坑指南
- 被包裹的必须是同步执行的 setState: 你不能在 startTransition 里面去写 setTimeout 或等待 fetch 请求。 (注意:React 19 对此做出了升级,React 19 的 Action 机制中,startTransition 内部已经可以支持异步 async/await 函数了,会自动等待 Promise 结束!)
- 不能用在受控组件的 value 绑定上: 如果你把输入框的 setQuery 丢进 startTransition,那用户打字时,输入框本身就会变得不跟手、产生严重的滞后。
- 小白解惑:
useTransition 是对“行为(函数调用)”的控制,而 useDeferredValue 是对“结果(状态数据)”的滞后。 说白了,useTransition 是让用户自己对任务的优先级进行控制(startTransition),useDeferredValue 是让程序来决定对任务优先级的控制(返回值state)。
- 这个useTransition和useDeferredValue都是为了把耗时的重度渲染降级为“低优先级任务”,从而保证用户界面的流畅响应,那么他们之间的区别是什么?
useInsertionEffect、useLayoutEffect
useInsertionEffect 的唯一目的,就是在所有的 DOM 节点发生改变(Mutation)之前触发。 这个hook就是在dom更新前的一个钩子函数,但是不要在这个钩子函数中拿dom,因为此时只是到了 虚拟dom 计算完成的阶段,还未执行dom节点更新,如果需要拿dom,可以使用 useLayoutEffect 钩子
执行顺序:useInsertionEffect -> DOM 节点更新 -> useLayoutEffect -> 浏览器绘制 (Paint) -> useEffect
它的语法和 useEffect 一模一样,也支持依赖项数组:
jsuseInsertionEffect(() => {
// 插入样式的逻辑
return () => {
// 清理样式的逻辑,执行时机和 useEffect类似:依赖数组变更时、组件卸载时
};
}, [dependencies]);
- 代码示例
jsimport { useInsertionEffect } from 'react';
// 一个非常简易的动态高亮组件
export default function HighlightText({ color }) {
// 💡 在 DOM 发生变化之前,就把动态 CSS 注入到页面中
useInsertionEffect(() => {
const styleId = `dynamic-style-${color}`;
// 如果已经存在该样式,则无需重复注入
if (!document.getElementById(styleId)) {
const styleElement = document.createElement('style');
styleElement.id = styleId;
styleElement.textContent = `.text-${color} { color: ${color}; font-weight: bold; }`;
document.head.appendChild(styleElement);
}
// 组件卸载时,清理掉注入的样式
return () => {
const styleElement = document.getElementById(styleId);
if (styleElement) {
styleElement.remove();
}
};
}, [color]); // 只有当颜色改变时,才重新计算和注入样式
return <p className={`text-${color}`}>我是动态注入样式的文本</p>;
}
- 虚拟DOM、DOM更新、屏幕绘制
- 虚拟DOM:在 react 发生状态变更时,diff阶段后,生成的AST。
- DOM更新:AST挂载后,会立刻调用浏览器的标准 DOM API(如 document.createElement, appendChild)把最新的节点挂载到 HTML 树上。此时就可以获取DOM了。只是页面上还没呈现。
- 屏幕绘制:浏览器在内存中已经知道了这棵 DOM 树长什么样,也计算好了样式,但还没有把颜色和像素真正画到你眼前的显示器屏幕上。
因此,合理的使用 useLayoutEffect 可以避免视觉差。例如下方的经典案例
jsimport { useState, useLayoutEffect, useRef } from 'react';
export default function AutoScaleText() {
const containerRef = useRef(null);
const [fontSize, setFontSize] = useState(16);
// 💡 此时 DOM 已经更新,但屏幕还没画,正是测量宽高、修改样式不闪烁的最佳时机
useLayoutEffect(() => {
if (containerRef.current) {
const width = containerRef.current.offsetWidth;
// 如果容器太窄,在绘制前赶紧把字号调小
if (width < 200) setFontSize(12);
}
}, []); // 仅在挂载时测量一次
return (
<div ref={containerRef} style={{ width: '150px', border: '1px solid black' }}>
<p style={{ fontSize: `${fontSize}px` }}>我很长我很长我很长我很长</p>
</div>
);
}
- 小白专属解惑
- 依赖项的作用是什么?既然这两个钩子都是监听dom在update 状态下的变更?为什么还需要依赖项。
“DOM 更新” 和 “组件更新(渲染)” 并不是等价的。依赖项存在的唯一目的,就是为了防止高频、无意义的“DOM 更新前夕/后夕”的重复执行,从而保护浏览器的性能。
- 依赖项的作用是什么?既然这两个钩子都是监听dom在update 状态下的变更?为什么还需要依赖项。
jsuseLayoutEffect(() => {
// 假设这是一个没有依赖项的 LayoutEffect
const width = ref.current.offsetWidth;
// ❌ 只要组件重新渲染,这就执行。执行了又触发 setState,
// setState 又导致组件重新渲染 -> 再次触发 DOM 更新 -> 再次执行这个 Hook
setBoxWidth(width);
}); // 👈 没传依赖项
js// 有依赖项的案例
function ProfileComponent() {
const [avatar, setAvatar] = useState('user.png'); // 状态1:头像
const [text, setText] = useState(''); // 状态2:输入框文字
const textRef = useRef(null);
// 💡 目标:我只想在“文字内容改变、导致文本框高度变高”的时候,去重新计算布局
useLayoutEffect(() => {
console.log("文本高度改变了,重新计算定位!");
adjustTooltipPosition(textRef.current.offsetHeight);
}, [text]); // 👈 只有 text 改变导致的 DOM 更新,才执行这里!
return (
<div>
<img src={avatar} onClick={() => setAvatar('new-user.png')} />
<textarea ref={textRef} value={text} onChange={e => setText(e.target.value)} />
</div>
);
}
- 避坑指南
- 不要在它里面读取 DOM 属性:在 useInsertionEffect 执行时,新一轮的 DOM 节点甚至还没挂载或更新。如果你在里面写了 el.getBoundingClientRect(),拿到的要么是旧的,要么会直接报错。
- 不要在它里面触发状态更新(State Update):千万不要在这里面调用 setState,这会扰乱 React 底层的并发渲染调度。
- 仅用于 CSS-in-JS 注入:如果你想请求数据、修改 DOM 属性、绑定全局事件,请老老实实回到 useEffect 或 useLayoutEffect。
useOptimistic
“乐观更新(Optimistic Updates)” 即不等接口返回,先假定它一定会成功,直接把 UI 变成成功后的状态。如果接口不幸失败了,再把 UI 回滚回去。
- 参数说明:
- passthroughState:标准状态(真数据)。通常是来自 useState、组件 props 或上层状态管理的数据。当没有异步操作在进行时,useOptimistic 返回的就是这个值。
- updateFn:更新函数。它是一个纯函数,接收两个参数:(currentOptimisticState, actionPayload),并返回你期待成功后的新状态。
- optimisticState:乐观状态(最终渲染在 UI 上的数据)。有异步操作时它是“假数据”,平时它是“真数据”。
- addOptimistic:触发器。调用它会立刻启动 updateFn 去更新 optimisticState。
- 代码示例:
jsimport { useOptimistic, useState } from "react";
// 模拟后端的异步发送接口
async function deliverMessageAPI(message) {
await new Promise((res) => setTimeout(res, 1500)); // 故意延迟 1.5 秒
return message;
}
export default function ChatApp() {
// 1. 真实存储的数据源(真数据)
const [messages, setMessages] = useState([
{ text: "你好!", sending: false }
]);
// 2. 绑定乐观更新 Hook
// messages 是基础数据,第二个参数定义了当“假装成功”时,数据怎么变
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessageText) => [
...state,
{ text: newMessageText, sending: true } // 👈 标记 sending: true 假装它已经发成功了
]
);
// 3. 表单提交的 Action
async function formAction(formData) {
const text = formData.get("message");
if (!text) return;
// 🔥 核心:不等接口,直接触发“乐观更新”,UI 列表立刻多出这一条
addOptimisticMessage(text);
try {
// 调用真正的后端接口
await deliverMessageAPI(text);
// 接口成功了!真正把数据同步到真实 state 里
setMessages((prev) => [...prev, { text, sending: false }]);
} catch (error) {
console.error("发送失败");
// 💡 重点:如果这里出错报错了,由于异步函数结束,
// useOptimistic 会自动丢弃刚才的“假数据”,自动回滚恢复原状,不需要你手动去删!
}
}
return (
<div>
{/* 渲染时,一定要绑定 optimisticMessages 乐观数据 */}
<div style={{ height: "200px", overflowY: "auto", border: "1px solid #ccc" }}>
{optimisticMessages.map((msg, index) => (
<p key={index} style={{ color: msg.sending ? "#999" : "#000" }}>
{msg.text} {msg.sending && <small>(发送中...)</small>}
</p>
))}
</div>
<form action={formAction}>
<input type="text" name="message" placeholder="输入消息..." />
<button type="submit">发送</button>
</form>
</div>
);
}
- 底层机制
- 当你把 formAction 绑定到表单上时,React 知道这是一个异步函数(Action)。
- 在 formAction 执行期间,只要这个异步函数 还没有结束(Promise 处于 pending 状态),useOptimistic 就会坚守岗位,一直用那个更新函数计算出来的“假数据”渲染 UI。
- 一旦你的异步函数执行完毕(无论是完成了 try 块中的 setMessages,还是跌进了 catch 块中崩掉),这个 Action 的生命周期就结束了。
- Action 一结束,useOptimistic 就会瞬间“隐退”,自动将控制权交还给真实的 messages 状态。
useSyncExternalStore
普通开发者在日常写业务代码时,可能 99% 的时间都不会直接用到它。但如果你在使用或编写 Redux、Zustand、MobX,或者需要直接监听浏览器原生状态(如窗口大小、网络在线状态),它就是绝对的底层大杀器。
重要:使用这个hooks时,一定要有外部订阅器才!!!
- 参数说明:
- subscribe:一个订阅函数。当外部数据改变时,它应该调用 React 传给它的回调,告诉 React:“数据变了,准备更新吧”。
- getSnapshot:一个读取函数。用来获取当前外部状态的快照。注意:如果数据没变,这个函数必须返回完全相同(引用相同)的值,否则会引发无限重复渲染。
- getServerSnapshot(可选):专门用于服务端渲染(SSR)或 Hydration 阶段获取初试数据的函数。
- 使用示例
js// 传统写法:每次都要写一堆 useEffect 绑定、解绑,而且多组件复用时容易不同步
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handle = () => setIsOnline(navigator.onLine);
window.addEventListener('online', handle);
window.addEventListener('offline', handle);
return () => { ... };
}, []);
js// 新的写法
import { useSyncExternalStore } from 'react';
// 1. 定义订阅逻辑(怎么监听数据变化)
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
// 返回一个清理函数,销毁时自动解绑
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
// 2. 定义如何获取当前的数据快照
function getSnapshot() {
return navigator.onLine; // 返回 true 或 false
}
export default function ConnectionStatus() {
// 3. 一行代码绑定,React 内部会自动处理并发、同步和销毁
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
return (
<div>
<h3>网络状态监控</h3>
<p>当前状态: {isOnline ? '🟢 已联网' : '🔴 已断网'}</p>
</div>
);
}
js// zustand 的案例说明
import { createStore } from 'zustand/vanilla'
import { useSyncExternalStore } from 'react'
// 1. 创建一个纯粹的外部原生 Store(完全脱离 React 掌控)
const vanillaStore = createStore((set) => ({
count: 0,
inc: () => set((state) => ({ count: state.count + 1 })),
}))
// 2. 封装我们自己的状态获取 Hook
export function useMyCount() {
return useSyncExternalStore(
// 🔗 订阅逻辑:vanillaStore.subscribe 接收一个回调,
// 当 count 改变时,Zustand 会自动触发这个回调通知 React
vanillaStore.subscribe,
// 📸 快照逻辑:告诉 React 怎么拿到最新值
() => vanillaStore.getState().count
)
}
- 小白解惑
- 其实这个hook就是,通过执行 subscribe 函数传递的 callback 来实现数据的变化的。只要执行了这个 callback ,那么就会引起数据的渲染。
- callback 函数,就是 React 框架暴露给外部的“手动刷新按钮”。任何时候,只要你(在外部)按下了这个按钮(执行了 callback()),React 就会立刻对该组件启动数据检查和潜在的渲染流程。
- 因此,回应上面提到的,一定要有外部的订阅器,来触发这个callback,才能使用这个hook。因此,其使用场景:Redux、Zustand、MobX,或者需要直接监听浏览器原生状态(如窗口大小、网络在线状态)