积累:06-react笔记

53 阅读19分钟

* 更新界面

  • useState
import {useState} from 'react';
function MyButton(){
    const [count,setCount] = useState(0)
}

  • 使用Hook:只能在组件(或者其他hook)的顶层

* state:组件的记忆

    const [index, setIndex] = useState(0);
    1. 组件进行第一次渲染。  因为你将 0 作为 index 的初始值传递给 useState,它将返回 [0, setIndex]。 React 记住 0 是最新的 state 值。
    1. 你更新了 state。当用户点击按钮时,它会调用 setIndex(index + 1)。 index 是 0,所以它是 setIndex(1)。这告诉 React 现在记住 index 是 1 并触发下一次渲染。
    1. 组件进行第二次渲染。React 仍然看到 useState(0),但是因为 React 记住 了你将 index 设置为了 1,它将返回 [1, setIndex]
    1. 以此类推!
state是隔离且私有的
  • 如果你渲染同一个组件两次,每个副本都会有完全隔离的 state!改变其中一个不会影响另一个。

* React渲染和提交

  • 三步:

      1. 触发一次渲染
      1. 渲染组件
      1. 提交到DOM
  • 步骤一:触发第一次渲染

    • 两种原因:组件初次渲染 和 组件状态发生变化
      1. 初次渲染:应用启动,调用createRoot方法并传入目标DOM节点,然后用你的组件调用render函数完成
    import Image from './Image.js';
    import { createRoot } from 'react-dom/client';
    
    const root = createRoot(document.getElementById('root'))
    root.render(<Image />);
    
    
      1. 状态更新时重新渲染
      • 通过使用 set 函数 更新其状态来触发之后的渲染。更新组件的状态会自动将一次渲染送入队列。
  • 步骤二:渲染组件

    • 在进行初次渲染时,  React 会调用根组件。
    • 对于后续的渲染,  React 会调用内部状态更新触发了渲染的函数组件。
    • 这个过程是递归的:如果更新后的组件会返回某个另外的组件,那么 React 接下来就会渲染 那个 组件,而如果那个组件又返回了某个组件,那么 React 接下来就会渲染 那个 组件,以此类推。这个过程会持续下去,直到没有更多的嵌套组件并且 React 确切知道哪些东西应该显示到屏幕上为止。
  • 步骤三:React把更改提交到DOM上

    • 对于初次渲染,React 会使用 appendChild() DOM API 将其创建的所有 DOM 节点放在屏幕上。
    • 对于重渲染,React 将应用最少的必要操作(在渲染时计算!),以使得 DOM 与最新的渲染输出相互匹配。

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
})

✅ 怎么调试状态变化?

  1. React DevTools
  • 检查组件的 props 和 state(在“组件”面板中)
  1. 函数式 setState + 打日志

    setCount(prev => {
      console.log('当前值:', prev)
      return prev + 1
    })
    
  2. 不要期望 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 都是新地址,会导致重复触发!

  • ✅ 解决方法:

  • 把它们用 useMemouseCallback 缓存起来

  • 或者提到组件外部声明不变

你可能不需要 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 复用逻辑

场景:当组件中重复使用类似 useEffectuseRef 或状态逻辑

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用法记忆口诀说明
useStateconst [x, setX] = useState(初始值)🧠**“记住值”**最常用,组件自己的状态,如输入框内容、开关等
useReducerconst [state, dispatch] = useReducer(函数, 初始值)⚙️**“集中管理复杂状态”**类似 Redux,适合对象/数组/多状态联动
useContextconst 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用法记忆口诀说明
useEffectuseEffect(() => {...}, [依赖])⏱️**“副作用 & 清理”**网络请求、订阅、定时器、DOM 操作等副作用逻辑
useLayoutEffectuseLayoutEffect(() => {...})🪞**“渲染前最后一刻”**useEffect 更早,适合布局测量、同步 DOM 读写
useInsertionEffectuseInsertionEffect(() => {...})🎨**“插入样式前”**样式库专用,几乎不用。仅在初始化插入 CSS 时用

ref 和 缓存Hook

Hook用法记忆口诀说明
useRefconst ref = useRef()📦**“保存盒子,不引起更新”**存数据 or 获取 DOM 引用,不会触发重渲染
useMemouseMemo(() => 计算, [依赖])🧮**“缓存计算结果”**避免重复计算,提高性能
useCallbackuseCallback(() => 函数, [依赖])🔁**“缓存函数定义”**避免函数重新生成,常配合 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用法记忆口诀说明
useTransitionconst [isPending, startTransition] = useTransition()🧘**“慢操作,别卡 UI”**把一些慢状态更新变“低优先级”,避免卡顿
useDeferredValueconst deferred = useDeferredValue(value)“值的慢版本”缓一缓输入(如搜索),提高体验
useIdconst 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()
useDebugValueuseDebugValue(value)🐛**“开发者调试信息”**显示在 React DevTools 中,仅用于自定义 Hook
useSyncExternalStoreuseSyncExternalStore(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 }
    
    • 工作流程:
        1. useStore 通过 useSyncExternalStore 订阅 store 的变化。
        1. initUserData 请求接口数据后,调用 setStoreData 更新全局 store。
        1. listeners.forEach(l => l()) 会通知所有订阅者刷新。
        1. 所有调用 useStore() 的组件都会重新渲染,展示最新数据。
    useSyncExternalStore 就是帮你写好了订阅和获取更新数据的模板,你只要提供两个函数:
     - 1. subscribe:怎么更新变化
     - 2. 怎么获取当前值
    

最新稳定新增(React 19)

Hook用法记忆口诀说明
useFormStatus在表单中使用📝**“表单提交状态”**结合 <form>,获取提交中/失败状态
useFormState[state, formAction] = useFormState()🪄**“表单状态 + 更新”**类似 useReducer,结合服务器端处理
useOptimisticconst [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 }
}

🧩 口诀“逻辑可复用,状态不外泄”