* 更新界面
useState
import {useState} from 'react';
function MyButton(){
const [count,setCount] = useState(0)
}
-
使用Hook:只能在组件(或者其他hook)的
顶层
* state:组件的记忆
const [index, setIndex] = useState(0);
-
- 组件进行第一次渲染。 因为你将
0作为index的初始值传递给useState,它将返回[0, setIndex]。 React 记住0是最新的 state 值。
- 组件进行第一次渲染。 因为你将
-
- 你更新了 state。当用户点击按钮时,它会调用
setIndex(index + 1)。index是0,所以它是setIndex(1)。这告诉 React 现在记住index是1并触发下一次渲染。
- 你更新了 state。当用户点击按钮时,它会调用
-
- 组件进行第二次渲染。React 仍然看到
useState(0),但是因为 React 记住 了你将index设置为了1,它将返回[1, setIndex]。
- 组件进行第二次渲染。React 仍然看到
-
- 以此类推!
state是隔离且私有的
- 如果你渲染同一个组件两次,每个副本都会有完全隔离的 state!改变其中一个不会影响另一个。
* React渲染和提交
-
三步:
-
- 触发一次渲染
-
- 渲染组件
-
- 提交到DOM
-
-
步骤一:触发第一次渲染
- 两种原因:组件初次渲染 和 组件状态发生变化
-
- 初次渲染:应用启动,调用createRoot方法并传入目标DOM节点,然后用你的组件调用render函数完成
import Image from './Image.js'; import { createRoot } from 'react-dom/client'; const root = createRoot(document.getElementById('root')) root.render(<Image />);-
- 状态更新时重新渲染
- 通过使用
set函数 更新其状态来触发之后的渲染。更新组件的状态会自动将一次渲染送入队列。
-
步骤二:渲染组件
- 在进行初次渲染时, React 会调用根组件。
- 对于后续的渲染, React 会调用内部状态更新触发了渲染的函数组件。
- 这个过程是递归的:如果更新后的组件会返回某个另外的组件,那么 React 接下来就会渲染 那个 组件,而如果那个组件又返回了某个组件,那么 React 接下来就会渲染 那个 组件,以此类推。这个过程会持续下去,直到没有更多的嵌套组件并且 React 确切知道哪些东西应该显示到屏幕上为止。
-
步骤三:React把更改提交到DOM上
- 对于初次渲染,React 会使用
appendChild()DOM API 将其创建的所有 DOM 节点放在屏幕上。 - 对于重渲染,React 将应用最少的必要操作(在渲染时计算!),以使得 DOM 与最新的渲染输出相互匹配。
- 对于初次渲染,React 会使用
state快照
-
定义:每次渲染时,组件都会获得一个“state 快照”副本。这个快照是只读的,不会随着异步更新实时变化。
-
示例说明:
function Counter() { const [count, setCount] = useState(0) function handleClick() { setCount(count + 1) setTimeout(() => { console.log('count in timeout:', count) // 🚨 不是最新值 }, 1000) } return <button onClick={handleClick}>Count: {count}</button> }- 点击按钮一次:
count变成 1,但打印出来的仍是旧值0。 - 因为 setTimeout 里访问的是旧的渲染快照。
- 点击按钮一次:
-
如何拿到新的值?
setTimeout(()=>{
setount(prev=>{
console.log('latest count:', prev)
return prev + 1
})
},1000)
把一系列state更新加入队列
- 更新队列:同一个事件循环中,React会把多次的setState 调用合并批处理,防止组件重复渲染
function Counter() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
setCount(count + 1)
setCount(count + 1)
}
return <button onClick={handleClick}>Count: {count}</button>
}
// 实际上只加了1.
- 正确的方式是:
setCount(prev => prev + 1)
setCount(prev => prev + 1)
setCount(prev => prev + 1)
// 这样才是加3
更新state中的对象
- 直接修改属性是无效的 , 必须创建新的对象
const [user, setUser] = useState({ name: 'Jack', age: 20 })
// ❌ 这样不会触发更新
user.age = 21
setUser(user)
// ✅ 正确方式
setUser(prev => ({
...prev,
age: 21
}))
- 原因:React是用 引用比较 来判断state是否变化,user对象引用不变->不会rerender
更新state中的数组
- 添加元素
setList(prev => [...pprev,newItem])
- 删除元素
setList(prev=>prev.filter(item.id !== targetId))
- 修改元素
setList(prev =>
prev.map(item => item.id === targetId ? { ...item, name: '新名' } : item)
)
!!!不要直接push/splice原数组
useReducer是否更适合管理复杂对象/数组?
- 是!当state结构复杂,更新逻辑多样时。useReducer 比 useState 更合适。
- !!! useReducer = 更强的useState + 可控的更新逻辑
为什么?
useState更适合管理单个值或简单对象。- 复杂结构(对象嵌套/数组组合)+ 多操作逻辑时,
useReducer提供了集中统一的逻辑管理(就像 Redux)
示例:管理一个 Todo 列表
type Todo = { id: number, text: string, done: boolean }
type Action =
| { type: 'add', payload: string }
| { type: 'toggle', payload: number }
| { type: 'delete', payload: number }
function reducer(state: Todo[], action: Action): Todo[] {
switch (action.type) {
case 'add':
return [...state, { id: Date.now(), text: action.payload, done: false }]
case 'toggle':
return state.map(todo =>
todo.id === action.payload ? { ...todo, done: !todo.done } : todo
)
case 'delete':
return state.filter(todo => todo.id !== action.payload)
default:
return state
}
}
const [todos, dispatch] = useReducer(reducer, [])
// 使用,调用useReducer返回的dispatch去执行reducer
dispatch({ type: 'add', payload: '学习 React' }) // payload是额外的参数,和业务相关
// 参数说明:
- reducer:需要自己写函数,定义了 状态如何根据动作(action)更新。接收两个参数(state,action)
- `state`:当前的状态
- `action`:一个对象(通常有 `type` 和 `payload`),描述“要干什么”
- `[]` 是初始状态 initialState; 这里是空数组,表示todo一开始是一个空列表
### 返回的两个值:
- `todos`
- **当前状态**(类似 `useState` 的 `state`)。
- 这里就是你的“待办事项数组”。
- `dispatch`
- 一个函数,你调用它来 **触发状态更新**。
- 它会把你传进去的 `action` 扔给 `reducer`,由 `reducer` 决定新的状态。
- 状态不可变更新逻辑集中,避免副作用、代码更可维护
immer 如何让你“看起来”可以直接修改对象?
- immer 利用了proxy 在幕后生成不可变数据,但是允许你使用“可变语法”。
原始写法(手动不可变)
setState(prev => ({
...prev,
user: {
...prev.user,
age: prev.user.age + 1
}
}))
- 使用
immer
import produce from 'immer';
setState(prev =>
produce(prev,draft =>{
draft.user.age+=1; // 看起来像是直接改的
})
)
推荐场景:
- 深嵌套结构的更新
- 多层数组+对象混合
- 搭配
useReducer(超搭配):
function reducer(state, action) {
return produce(state, draft => {
// 修改 draft 即可
})
}
React 中state为何是异步的?怎么调试?
📌 一句话解释:
为了性能优化,React 会把多次 setState 操作合并(批处理)后再执行。
🧠 示例:
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
console.log(count) // 🚨 总是旧值!
}
🧪 实质:
setCount(count + 1)其实是“调度了一个更新任务”- 当前事件循环中不会立刻执行更新,而是等所有任务合并后再批量渲染(Fiber 的协调阶段)
✅ 如何获取“最新值”?
使用函数式更新:
setCount(prev => {
console.log('最新值:', prev)
return prev + 1
})
✅ 怎么调试状态变化?
- React DevTools
- 检查组件的 props 和 state(在“组件”面板中)
-
函数式 setState + 打日志
setCount(prev => { console.log('当前值:', prev) return prev + 1 }) -
不要期望 setState 后立即拿到新值
setCount(count + 1) console.log(count) // 依然是旧值
在组件间共享状态
核心概念:状态提升
- 将相关state从组件中移除
- 将state投生到最近的公共父组件
- 父组件通过props向子组件传递状态和状态变更逻辑
使用Context + Reducer 实现跨组件共享状态
- 当状态要被多个不相邻组件访问或者修改时,推荐结合:
useReducer:集中管理复杂状态和状态更新逻辑Context:在组件树中注入状态与派发函数
- 场景举例:
// 1. 创建上下文
const TaskContext = React.createContext();
const TasksDispatchContext = React.createContext()
// 2. 定义reducer
function tasksReducer(tasks,action){
switch (action.type) {
case 'add':
return [...tasks, { id: Date.now(), text: action.payload, done: false }]
case 'toggle':
return tasks.map(t =>
t.id === action.payload ? { ...t, done: !t.done } : t
)
default:
return tasks
}
}
// 3. Provider 组件包裹全局
function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, [])
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
)
}
// 4. 子组件中消费:
const tasks = useContext(TasksContext)
const dispatch = useContext(TasksDispatchContext)
✅ 好处:
- 中央化状态与逻辑
- 可在任意深度组件中访问与更新
- 更清晰、更可维护的架构
✅ 2. 使用自定义 Hook(Custom Hook)复用逻辑
当你有多个组件中重复使用类似逻辑(例如:输入处理、表单验证、轮询等),可以提取出自定义 Hook。
💡 示例:复用输入状态逻辑
function useInput(initialValue = '') {
const [value, setValue] = useState(initialValue)
const onChange = e => setValue(e.target.value)
return { value, onChange, reset: () => setValue('') }
}
function Form() {
const name = useInput()
const email = useInput()
return (
<>
<input {...name} placeholder="Name" />
<input {...email} placeholder="Email" />
</>
)
}
✅ 好处:
- 更高的可读性
- 逻辑复用而不重复
- 独立状态依然位于组件中,不会全局污染
✅ 3. Illustrative Example:完整状态提升实践
以“城市天气面板”为例,我们实现多个 Panel 点击切换详情,且最多只有一个展开。
💡 目标:
- 多个城市 Panel
- 只能展开一个(类似手风琴)
- 点击某个 Panel,会让其他 Panel 收起
示例代码:
function Accordion() {
const [activeId, setActiveId] = useState(null)
return (
<>
<Panel
id="london"
title="London"
isActive={activeId === 'london'}
onShow={() => setActiveId('london')}
>
Capital of UK.
</Panel>
<Panel
id="paris"
title="Paris"
isActive={activeId === 'paris'}
onShow={() => setActiveId('paris')}
>
Capital of France.
</Panel>
</>
)
}
function Panel({ title, children, isActive, onShow }) {
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? (
<>
<p>{children}</p>
<button disabled>Active</button>
</>
) : (
<button onClick={onShow}>Show</button>
)}
</section>
)
}
✅ 技术点总结:
- 状态提升:
activeId在父组件中,统一调度多个子组件状态 - 子组件通过 props 控制行为(是否显示、响应点击)
- 单向数据流:React 状态永远从上而下流动
🔚 总结对比
| 技术 | 用于场景 | 特点/优点 |
|---|---|---|
| Context + Reducer | 跨层级共享状态、统一逻辑 | 中央管理状态,避免 prop drilling |
| Custom Hook | 抽离通用逻辑、局部状态复用 | 独立封装逻辑,保持状态局部 |
| 状态提升 | 父组件统一控制兄弟组件状态 | 保持状态同步,简洁可靠 |
使用 ref 引用值
- 非渲染相关的数据容器
- 场景:存储组件生命周期中的“可变值”而不触发重新渲染
const countRef = useRef(0); countRef.current+=1; // 不会触发重新渲染- ref.current 可跨渲染持续存在
- 用于:
- 跟踪调用次数
- 保存定时器ID
- 缓存上一次值(如同于手动实现usePrevious)
使用 ref 操作 DOM
- 场景:访问或操作实际 DOM 节点(焦点控制、滚动、测量等)
const inputRef = useRef(null)
useEffect(() => {
inputRef.current.focus()
}, [])
ref是连接 JSX 和原生 DOM 的桥梁- 用于:
- 聚焦 input
- 手动滚动/测量位置
- 播放/暂停视频、音频等
使用 Effect 进行同步 (数据同步/订阅/清理)
- 常见用途:
- 监听props/state变化并作出响应
- 设置定时器、事件监听
- 发起异步请求、订阅websocket
useEffect(()=>{
const id = setInterval(() => console.log('tick'), 1000)
return () => clearInterval(id)
},[]) // 依赖空数组:只运行一次
依赖情况一览表(最常见的 3 种)
| 写法 | 依赖 | 什么时候运行 |
|---|---|---|
useEffect(() => { ... }) | 无依赖 | 每次组件更新(包括初始挂载)都运行 |
useEffect(() => { ... }, []) | 空数组 | 只在组件挂载时运行一次 |
useEffect(() => { ... }, [a, b]) | 有依赖 | 初次挂载 + a 或 b 发生变化时运行 |
- !!第四种:依赖是对象/数组/函数要小心
const obj = { x: 1 };
useEffect(() => {
console.log('每次都运行?');
}, [obj]); // ❌ 即使 obj 内容没变,但引用地址变了
-
对象、数组、函数是引用类型,每次 render 都是新地址,会导致重复触发!
-
✅ 解决方法:
-
把它们用
useMemo或useCallback缓存起来 -
或者提到组件外部声明不变
你可能不需要 Effect
场景:避免滥用 useEffect 做可以直接计算或响应的事情
❌ 错误做法:
tsx
useEffect(() => {
setDerivedValue(a + b)
}, [a, b])
✅ 正确做法:
tsx
const derivedValue = a + b // 用表达式或 memo 更清晰
📌 原则:Effect 是副作用,不是表达式逻辑替代品
响应式 Effect 的生命周期
| 生命周期阶段 | 描述 |
|---|---|
| 初次渲染后 | 执行 useEffect 的回调 |
| 任意依赖值更新后 | 清除旧 effect → 运行新回调 |
| 卸载组件时 | 执行 return 中的清理函数 |
将事件从 Effect 中分开
- 如果事件逻辑与生命周期无关,放进useEffect会造成过度依赖、不可预测行为
移除 Effect 依赖(使用 ref 或封装逻辑)
场景:Effect 中使用的函数对象或值常常变动,导致不必要的重跑
tsx
复制编辑
const handlerRef = useRef(handler) // 保存函数引用
useEffect(() => {
const listener = () => handlerRef.current()
window.addEventListener('scroll', listener)
return () => window.removeEventListener('scroll', listener)
}, []) // 安全移除依赖
使用自定义 Hook 复用逻辑
场景:当组件中重复使用类似 useEffect、useRef 或状态逻辑
function useInterval(callback, delay) {
const savedCallback = useRef(callback)
useEffect(() => {
savedCallback.current = callback
})
useEffect(() => {
const id = setInterval(() => savedCallback.current(), delay)
return () => clearInterval(id)
}, [delay])
}
📌 自定义 Hook 的价值:
- 让逻辑可以像组件一样被“复用”
- 不引入额外的状态
- 组合式设计,提升可维护性
# React 内置 Hook
核心状态 Hook(状态管理三巨头)
| Hook | 用法 | 记忆口诀 | 说明 |
|---|---|---|---|
useState | const [x, setX] = useState(初始值) | 🧠**“记住值”** | 最常用,组件自己的状态,如输入框内容、开关等 |
useReducer | const [state, dispatch] = useReducer(函数, 初始值) | ⚙️**“集中管理复杂状态”** | 类似 Redux,适合对象/数组/多状态联动 |
useContext | const value = useContext(MyContext) | 🌍**“从大局获取状态”** | 跨层组件共享状态,替代 props 一层层传 |
// useState
import {useState} from 'react';
function Counter(){
const [counter,setCounter] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
// useEffect
import {useState,useEffect} from 'react';
function Timer(){
const [seconds,setSeconds] = useState(0);
useEffect(()=>{
},[]) // 空数组,只执行一次
}
// useContext 【跨层级共享数据的捷径】 ### `useContext(SomeContext)`
// 第一步:创建一个上下文
const UserContext = React.createContext();
// 第二步:在最外层组件调用<Provider> 提供数据
<UserContext.Provider value={{name:'Alice'}}>
<App/>
</UserContext>
// 第三步:任意的子组件使用useContext获取数据
const use = useContext(UserContext)
//console.log(user.name); // 输出 Alice
💡 什么时候用useContext?
| 场景 | 是否适合用 useContext |
|---|---|
| 登录用户信息(user) | ✅ 非常合适 |
| 当前主题(暗色/亮色) | ✅ 非常合适 |
| 当前语言(多语言切换) | ✅ 非常合适 |
| 短暂状态(表单输入、计数) | ❌ 不推荐 |
生命周期 & 副作用Hook
| Hook | 用法 | 记忆口诀 | 说明 |
|---|---|---|---|
useEffect | useEffect(() => {...}, [依赖]) | ⏱️**“副作用 & 清理”** | 网络请求、订阅、定时器、DOM 操作等副作用逻辑 |
useLayoutEffect | useLayoutEffect(() => {...}) | 🪞**“渲染前最后一刻”** | 比 useEffect 更早,适合布局测量、同步 DOM 读写 |
useInsertionEffect | useInsertionEffect(() => {...}) | 🎨**“插入样式前”** | 样式库专用,几乎不用。仅在初始化插入 CSS 时用 |
ref 和 缓存Hook
| Hook | 用法 | 记忆口诀 | 说明 |
|---|---|---|---|
useRef | const ref = useRef() | 📦**“保存盒子,不引起更新”** | 存数据 or 获取 DOM 引用,不会触发重渲染 |
useMemo | useMemo(() => 计算, [依赖]) | 🧮**“缓存计算结果”** | 避免重复计算,提高性能 |
useCallback | useCallback(() => 函数, [依赖]) | 🔁**“缓存函数定义”** | 避免函数重新生成,常配合 memo 组件优化渲染 |
useRef---像一个盒子-
特点:
- 值存在.current里
- 改变他不会触发组件渲染
- 常用来保存DOM或者保存某个可变值(定时器id,历史数据等)
function App(){ const countRef = useRef(0) const handleClick =()=>{ countRef.current +=1; console.log(countRef.current); // 永远保存最新值 } return <button onClick={handleClick}>+1</button>; } // 类似记事本,你要写啥都帮你存着,但是不会喊“我变了”
-
useMeno---记住值(计算结果)- 特点:
- 用来缓存计算结果
- 依赖没有变,不会重新计算
- 适合耗时计算或避免不必要的重新渲染
const expensiveValue = useMemo(()=>{ console.log('计算中...'); return someBigArray.filter(item => item.active); },[someBigArray]) // 条件一样就直接抄,不要重复计算
- 特点:
useCallback---记住函数- 返回一个缓存的函数引用
- 依赖没变,函数地址也不会改变
- 常用于把函数传给子组件,避免子组件重复渲染
示例:不用 useCallback
import { useState } from 'react';
// 子组件
function Child({ onClick }) {
console.log('子组件渲染');
return <button onClick={onClick}>子组件按钮</button>;
}
// 父组件
function App() {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log('点击了');
};
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<Child onClick={handleClick} />
</div>
);
}
👉 每次点击 +1,父组件重新渲染,handleClick 被重新创建,Child 也会 重新渲染。
5. 用 useCallback 优化
import { useState, useCallback, memo } from 'react';
const Child = memo(({ onClick }) => {
console.log('子组件渲染');
return <button onClick={onClick}>子组件按钮</button>;
});
function App() {
const [count, setCount] = useState(0);
// 缓存函数引用
const handleClick = useCallback(() => {
console.log('点击了');
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<Child onClick={handleClick} />
</div>
);
}
👉 现在点击 +1,父组件重新渲染,但是 handleClick 函数引用不变,Child 不会重新渲染。
新的资源管理 & 异步 Hook(React 18+)
| Hook | 用法 | 记忆口诀 | 说明 |
|---|---|---|---|
useTransition | const [isPending, startTransition] = useTransition() | 🧘**“慢操作,别卡 UI”** | 把一些慢状态更新变“低优先级”,避免卡顿 |
useDeferredValue | const deferred = useDeferredValue(value) | ⏳ “值的慢版本” | 缓一缓输入(如搜索),提高体验 |
useId | const id = useId() | 🆔**“唯一 id 生成器”** | 用于无重复 id,例如 label & input |
useTransition---任务的优先级低,把更新分成“急的”和“不急的”
import {useState,useTransition} from 'react';
export default function App(){
const [query , setQuery] = useState("");
const [list, setList] = useState([]);
const [isPending,startTransition] - useTransition();
// isPending 表示低优先级的任务是否还在进行
const handleInputChange=(e)=>{
const value = e.target.value;
setQuery(value) // 立即更新输入框,优先级高
startTransition(()=>{
// 筛选,优先级低
const newList = Array(20000)
.fill(0)
.map((_, i) => value + i);
setList(newList);
})
};
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <p>加载中...</p>}
<ul>
{list.map((item, i) => <li key={i}>{item}</li>)}
</ul>
</>
);
}
useDeferredValue---延迟一个值的更新- 场景: 搜索框输入时,输入框文字要立即更新,但列表过滤的值可以延迟一点更新,避免频繁计算。
import { useState, useDeferredValue } from "react";
export default function App() {
const [text, setText] = useState("");
const deferredText = useDeferredValue(text); // 延迟版本的 text
const list = Array(20000)
.fill(0)
.map((_, i) => deferredText + i);
return (
<>
<input value={text} onChange={(e) => setText(e.target.value)} />
<ul>
{list.map((item, i) => <li key={i}>{item}</li>)}
</ul>
</>
);
}
// useDeferredValue(value) 会返回一个延迟更新的value
// UI 中直接显示的部分可以用 `value`,耗时的渲染用 `deferredValue`,提高流畅度
useId---生成唯一且稳定的ID- 唯一性:每次渲染都返回相同的ID,不会和页面上的组件冲突
-
- SSR/CSR 一致:在服务器渲染和客户端渲染之间保证 ID 一致,避免 hydration 报错。
- 不依赖全局变量:不用自己维护自增 ID。
import {useId} from 'react';
export default function myForm(){
const id = useId()
return (
<div>
<label htmlFor={id}>用户名:</label>
<input id={id} type="text" />
</div>
)
}
// 有什么用?
// 假设写了一个表单组件,被渲染多次,如果手写id="username",多个相同 ID 会冲突。
`useId()` 保证每个组件实例的 ID 唯一。
控制组件状态 Hook
| Hook | 用法 | 记忆口诀 | 说明 |
|---|---|---|---|
useImperativeHandle | 配合 forwardRef() 使用 | 🛠️**“给父组件暴露方法”** | 自定义 ref 的暴露内容,如给父组件暴露 .scrollToBottom() |
useDebugValue | useDebugValue(value) | 🐛**“开发者调试信息”** | 显示在 React DevTools 中,仅用于自定义 Hook |
useSyncExternalStore | useSyncExternalStore(subscribe, getSnapshot) | 🔄**“监听外部状态”** | 监听全局状态库,如 Redux、Zustand 等 |
useImperativeHandle---把遥控器交出去- 给父组件一个“遥控器”,让父组件能直接调用子组件里的方法/属性
import { useImperativeHandle, useRef, forwardRef } from 'react';
const Child = forwardRef((props, ref) => {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
// 把 focusInput 方法暴露给父组件
focusInput: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} />;
});
export default function Parent() {
const childRef = useRef(null);
return (
<>
<Child ref={childRef} />
<button onClick={() => childRef.current.focusInput()}>
让子组件的输入框获得焦点
</button>
</>
);
}
总结
-
forwardRef = 让父组件拿到子组件的控制权(把 ref 传进来)。
-
useImperativeHandle = 精确控制暴露给父组件的内容(不直接暴露 DOM,而是包装成方法)。
-
useDebugValue---“调试标签”- 给自定义Hook加个标签,方便在React DevTools 里调试
- 它不影响代码功能,只是让 DevTools 里看起来更清晰。
- 就像在调试面板里给变量贴上小纸条。
-
useSyncExternalStore---同步外部数据源- 确保React组件和外部数据(Redux,全局store,浏览器API)保持同步,并且支持SSR
- React 自家做的“监听外部数据变化的安全方式”。
- 以前我们用
useEffect + subscribe,但可能有并发渲染问题。 - 这个 Hook 官方推荐替代方案,尤其适合状态管理库。
// 1. 初始化从接口获取数据 // 2. 任意地方更新store // 3. 所有使用useSyncEternalStore的组件都会自动更新 // store.js let storeData = { user: null } let listeners = [] export function getSnapshot() { return storeData } export function subscribe(listener) { listeners.push(listener) return () => { listeners = listeners.filter(l => l !== listener) } } export function setStoreData(partial) { storeData = { ...storeData, ...partial } listeners.forEach(l => l()) } export async function initUserData() { // 模拟请求接口 const res = await fetch('https://jsonplaceholder.typicode.com/users/1') const data = await res.json() setStoreData({ user: data }) }// useStore.js import { useSyncExternalStore } from 'react' import { subscribe, getSnapshot, initUserData } from './store' export function useStore(){ return useSyncExternalStore(subscribe , getSnapshot) } export { initUserData }- 工作流程:
-
useStore通过useSyncExternalStore订阅store的变化。
-
initUserData请求接口数据后,调用setStoreData更新全局 store。
-
listeners.forEach(l => l())会通知所有订阅者刷新。
-
- 所有调用
useStore()的组件都会重新渲染,展示最新数据。
- 所有调用
-
useSyncExternalStore就是帮你写好了订阅和获取更新数据的模板,你只要提供两个函数:- 1. subscribe:怎么更新变化 - 2. 怎么获取当前值
最新稳定新增(React 19)
| Hook | 用法 | 记忆口诀 | 说明 |
|---|---|---|---|
useFormStatus | 在表单中使用 | 📝**“表单提交状态”** | 结合 <form>,获取提交中/失败状态 |
useFormState | [state, formAction] = useFormState() | 🪄**“表单状态 + 更新”** | 类似 useReducer,结合服务器端处理 |
useOptimistic | const [value, update] = useOptimistic() | 🧙**“先让用户看到再说”** | 先乐观更新 UI,再同步真实状态(如点赞数) |
useActionState | 与 server action 配合 | 🧬**“服务器更新状态”** | 配合 Server Action 处理异步状态 |
useFormStatus--- 表单现在在干嘛- 让子组件知道表单提交的状态(loading / error / success)。
'use client'
import {useFormStatus} from 'react-dom';
function SubmitButton() {
// 把这个组件放在form里,就能知道这个form的状态
const { pending } = useFormStatus()
return <button disabled={pending}>{pending ? '提交中...' : '提交'}</button>
}
export default function MyForm() {
async function action(formData) {
await new Promise(r => setTimeout(r, 2000)) // 模拟请求
}
return (
<form action={action}>
<input name="name" />
<SubmitButton />
</form>
)
}
useFormState--- 和表单共享状态- 把表单的提交结果同步给组件
'use client'
import {useFoemState} from 'react-dom';
async function createUser(prevState, formData) {
if (!formData.get('name')) {
return { error: '名字不能为空' }
}
return { message: '创建成功' }
}
export default function MyForm() {
const [state, formAction] = useFormState(createUser, { message: '', error: '' })
return (
<form action={formAction}>
<input name="name" />
<button>提交</button>
{state.error && <p style={{ color: 'red' }}>{state.error}</p>}
{state.message && <p style={{ color: 'green' }}>{state.message}</p>}
</form>
)
}
useOptimistic--- 假装已经成功了
提交表单/请求时,先“假装”成功更新 UI,让用户感觉更快,再等真实结果回来。
📖 记忆法
- 就像外卖 APP,你刚下单,页面立刻显示“骑手已接单”,虽然实际上骑手可能还没接到。
📌 例子
tsx
复制编辑
'use client'
import { useOptimistic } from 'react'
export default function Comments() {
const [comments, addCommentOptimistic] = useOptimistic(
[], // 初始评论列表
(state, newComment) => [...state, { text: newComment, id: Date.now() }]
)
async function handleSubmit(formData) {
const comment = formData.get('comment')
addCommentOptimistic(comment) // 假装已经加上了
await new Promise(r => setTimeout(r, 2000)) // 模拟服务器保存
}
return (
<form action={handleSubmit}>
<input name="comment" />
<button>发送</button>
<ul>
{comments.map(c => <li key={c.id}>{c.text}</li>)}
</ul>
</form>
)
}
useActionState —— “动作的状态管理器”
绑定一个异步动作(Server Action)并管理它的 loading、错误、结果状态。
📖 记忆法
useFormState专注于 表单,而useActionState更通用,适用于任何异步操作(按钮点击、表单提交、删除数据等)。
📌 例子
tsx
复制编辑
'use client'
import { useActionState } from 'react'
async function saveName(prevState, formData) {
await new Promise(r => setTimeout(r, 2000))
return { savedName: formData.get('name') }
}
export default function MyForm() {
const [state, formAction, isPending] = useActionState(saveName, { savedName: '' })
return (
<form action={formAction}>
<input name="name" />
<button disabled={isPending}>{isPending ? '保存中...' : '保存'}</button>
{state.savedName && <p>保存成功:{state.savedName}</p>}
</form>
)
}
💡 一句话:管理“任意动作”的异步状态,不限于表单。
🎯 总结表
| Hook | 比喻 | 用途 | 适用场景 |
|---|---|---|---|
useFormStatus | 进度指示灯 | 知道表单是否在提交中 | 提交按钮、loading 状态 |
useFormState | 结果收件箱 | 获取表单提交后的结果/错误 | 表单校验、显示反馈 |
useOptimistic | 假装成功 | 提前更新 UI | 评论、点赞、购物车 |
useActionState | 动作状态管理器 | 管理任意异步操作的状态 | 不限于表单 |
自定义 Hook
你可以用 useXXX 命名自己的 Hook:
function useCounter() {
const [count, setCount] = useState(0)
const inc = () => setCount(c => c + 1)
return { count, inc }
}
🧩 口诀: “逻辑可复用,状态不外泄”