跳到主要内容

React Hooks

本章将介绍 React Hooks,这是 React 16.8 引入的新特性,允许在函数组件中使用状态和其他 React 特性。Hooks 改变了我们编写 React 的方式,使得逻辑复用更加简单且直观。

什么是 Hooks?

Hooks 是让你在函数组件中"钩入" React 状态和生命周期等特性的函数。它们的名字通常以 use 开头,例如 useStateuseEffect

在 Hooks 出现之前,函数组件无法拥有状态,生命周期只能在类组件中使用。这导致组件逻辑分散在不同的生命周期方法中,难以复用和维护。Hooks 的出现解决了这些问题,让函数组件拥有了完整的能力。

Hooks 的核心准则

  1. 只在顶层调用 Hook:不要在循环、条件或嵌套函数中调用 Hook。React 依赖调用顺序来正确匹配状态,如果在条件语句中调用,顺序可能会改变。
  2. 只在 React 函数中调用 Hook:不要在普通 JavaScript 函数中调用(除自定义 Hook 之外)。
// ❌ 错误:在条件语句中调用 Hook
function Component({ shouldFetch }) {
if (shouldFetch) {
useEffect(() => { fetchData(); }, []); // 顺序不确定!
}
// ...
}

// ✅ 正确:始终在顶层调用
function Component({ shouldFetch }) {
useEffect(() => {
if (shouldFetch) {
fetchData();
}
}, [shouldFetch]);
// ...
}

useState:管理状态

useState 是最常用的 Hook,用于在函数组件中添加状态。它返回一个包含两个元素的数组:当前状态值和更新状态的函数。

基本用法

实时编辑器
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div style={{ padding: '20px', border: '1px solid #ddd', borderRadius: '8px' }}>
      <p>当前计数: <strong style={{ color: '#2563eb' }}>{count}</strong></p>
      <div style={{ display: 'flex', gap: '10px' }}>
        <button 
          onClick={() => setCount(count + 1)}
          style={{ padding: '8px 16px', background: '#2563eb', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
        >
          增加
        </button>
        <button 
          onClick={() => setCount(0)}
          style={{ padding: '8px 16px', background: '#ef4444', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
        >
          重置
        </button>
      </div>
    </div>
  );
}
结果
Loading...

函数式更新

当新状态依赖于之前的状态时,推荐传递一个函数给 set 函数。这可以避免在闭包中捕获旧的状态值。

// 推荐做法:即使在异步操作中也能获取最新状态
setCount(prevCount => prevCount + 1);

这种写法在处理多次连续更新时特别重要。React 会将更新排入队列,确保每次更新都基于最新的状态:

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

const handleClick = () => {
// ❌ 错误:三个调用都是基于同一个旧值
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// 最终 count 只增加 1

// ✅ 正确:每个更新都基于前一个更新的结果
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
// 最终 count 增加 3
};
}

惰性初始化

如果初始状态需要复杂计算,可以传递一个函数给 useState,它只在首次渲染时执行:

// ❌ 每次渲染都会调用 expensiveCalculation()
const [state, setState] = useState(expensiveCalculation());

// ✅ 只在首次渲染时调用
const [state, setState] = useState(() => expensiveCalculation());

更新对象和数组

状态应该是不可变的。更新对象或数组时,需要创建新的引用:

// 更新对象
const [user, setUser] = useState({ name: '张三', age: 25 });
setUser({ ...user, age: 26 }); // 创建新对象

// 更新数组
const [items, setItems] = useState([1, 2, 3]);
setItems([...items, 4]); // 添加元素
setItems(items.filter(item => item !== 2)); // 删除元素

useEffect:处理副作用

useEffect 用于处理副作用,如数据获取、订阅、手动修改 DOM 等。它在组件渲染完成后执行。

执行时机

useEffect 在浏览器完成绘制后异步执行,不会阻塞浏览器渲染。如果副作用需要同步执行(如测量 DOM),应使用 useLayoutEffect

实时编辑器
function Timer() {
  const [seconds, setSeconds] = useState(0);
  const [isActive, setIsActive] = useState(false);

  useEffect(() => {
    let interval = null;
    if (isActive) {
      interval = setInterval(() => {
        setSeconds(s => s + 1);
      }, 1000);
    }
    // 清理函数:组件卸载或依赖变化前执行
    return () => clearInterval(interval);
  }, [isActive]); // 仅在 isActive 变化时重新运行

  return (
    <div style={{ padding: '10px' }}>
      <h3>计时器: {seconds}</h3>
      <button onClick={() => setIsActive(!isActive)}>
        {isActive ? '暂停' : '开始'}
      </button>
      <button onClick={() => setSeconds(0)} style={{ marginLeft: '10px' }}>
        重置
      </button>
    </div>
  );
}
结果
Loading...

依赖数组的作用

依赖数组决定了 useEffect 何时重新执行:

// 空数组:只在挂载时执行一次
useEffect(() => {
console.log('组件挂载');
}, []);

// 有依赖:依赖变化时执行
useEffect(() => {
console.log('count 变化了:', count);
}, [count]);

// 无依赖数组:每次渲染都执行
useEffect(() => {
console.log('每次渲染都执行');
});

清理函数

当副作用涉及订阅、定时器等资源时,需要在清理函数中释放:

useEffect(() => {
const subscription = someAPI.subscribe();
return () => subscription.unsubscribe(); // 清理
}, []);

常见模式:数据获取

function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
let cancelled = false; // 防止竞态条件

async function fetchUser() {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
if (!cancelled) {
setUser(data);
}
} catch (err) {
if (!cancelled) {
setError(err);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}

fetchUser();

return () => { cancelled = true; }; // 清理
}, [userId]); // userId 变化时重新获取

if (loading) return <div>加载中...</div>;
if (error) return <div>出错了</div>;
return <div>{user?.name}</div>;
}

useReducer:复杂状态管理

当状态逻辑复杂或涉及多个子值时,useReduceruseState 更合适。

实时编辑器
function TodoApp() {
  const initialState = { todos: [], filter: 'all' };

  function reducer(state, action) {
    switch (action.type) {
      case 'add':
        return {
          ...state,
          todos: [...state.todos, { id: Date.now(), text: action.text, done: false }]
        };
      case 'toggle':
        return {
          ...state,
          todos: state.todos.map(todo =>
            todo.id === action.id ? { ...todo, done: !todo.done } : todo
          )
        };
      case 'delete':
        return {
          ...state,
          todos: state.todos.filter(todo => todo.id !== action.id)
        };
      case 'setFilter':
        return { ...state, filter: action.filter };
      default:
        return state;
    }
  }

  const [state, dispatch] = useReducer(reducer, initialState);
  const [text, setText] = useState('');

  const filteredTodos = state.todos.filter(todo => {
    if (state.filter === 'active') return !todo.done;
    if (state.filter === 'completed') return todo.done;
    return true;
  });

  return (
    <div style={{ padding: '10px' }}>
      <div>
        <input 
          value={text} 
          onChange={e => setText(e.target.value)}
          placeholder="添加待办..."
        />
        <button onClick={() => { 
          if (text.trim()) {
            dispatch({ type: 'add', text });
            setText('');
          }
        }}>
          添加
        </button>
      </div>
      <div>
        <button onClick={() => dispatch({ type: 'setFilter', filter: 'all' })}>全部</button>
        <button onClick={() => dispatch({ type: 'setFilter', filter: 'active' })}>未完成</button>
        <button onClick={() => dispatch({ type: 'setFilter', filter: 'completed' })}>已完成</button>
      </div>
      <ul>
        {filteredTodos.map(todo => (
          <li key={todo.id} style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
            <span onClick={() => dispatch({ type: 'toggle', id: todo.id })}>
              {todo.text}
            </span>
            <button onClick={() => dispatch({ type: 'delete', id: todo.id })}>删除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}
结果
Loading...

useReducer 适合以下场景:

  • 状态包含多个子值
  • 下一个状态依赖上一个状态
  • 状态逻辑需要在组件外测试

useContext:跨组件传递数据

useContext 允许你订阅 React context 而不引入嵌套组件。它解决了"属性钻取"(prop drilling)问题。

实时编辑器
// 1. 创建 Context
const ThemeContext = React.createContext('light');

function App() {
  const [theme, setTheme] = useState('light');
  
  return (
    <ThemeContext.Provider value={theme}>
      <div style={{ 
        padding: '20px', 
        background: theme === 'dark' ? '#1e293b' : '#f8fafc',
        color: theme === 'dark' ? 'white' : 'black'
      }}>
        <Header />
        <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
          切换主题
        </button>
      </div>
    </ThemeContext.Provider>
  );
}

function Header() {
  // 2. 使用 Context
  const theme = useContext(ThemeContext);
  return <h4>当前主题: {theme}</h4>;
}
结果
Loading...

React 19:简化的 Provider 语法

在 React 19 中,可以直接使用 <Context> 代替 <Context.Provider>

// React 19 新语法
const ThemeContext = createContext('');

function App({ children }) {
return (
// 新语法:直接使用 Context 组件
<ThemeContext value="dark">
{children}
</ThemeContext>
);
}

// 旧语法仍然支持,但未来会被弃用
// <ThemeContext.Provider value="dark">
// {children}
// </ThemeContext.Provider>

迁移建议:React 官方提供了 codemod 工具来自动更新:

npx codemod@latest react/19/context-provider

配合 useReducer 实现全局状态

const StateContext = React.createContext();
const DispatchContext = React.createContext();

function AppProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);

return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}

// 在子组件中使用
function TodoList() {
const state = useContext(StateContext);
const dispatch = useContext(DispatchContext);
// ...
}

useRef:引用值与 DOM

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数。它有两个主要用途:

  1. 获取 DOM 元素
  2. 存储不需要触发渲染的变量(类似类组件的实例属性)。

获取 DOM 元素

实时编辑器
function TextInputWithFocusButton() {
  const inputEl = useRef(null);

  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };

  return (
    <div style={{ padding: '10px' }}>
      <input ref={inputEl} type="text" placeholder="点击按钮聚焦" />
      <button onClick={onButtonClick} style={{ marginLeft: '10px' }}>
        聚焦输入框
      </button>
    </div>
  );
}
结果
Loading...

存储可变值

useRef 可以存储任何可变值,且修改不会触发重新渲染:

function StopWatch() {
const [time, setTime] = useState(0);
const intervalRef = useRef(null); // 存储 interval ID

const start = () => {
if (intervalRef.current) return; // 防止重复启动
intervalRef.current = setInterval(() => {
setTime(t => t + 10);
}, 10);
};

const stop = () => {
clearInterval(intervalRef.current);
intervalRef.current = null;
};

const reset = () => {
stop();
setTime(0);
};

return (
<div>
<p>{(time / 1000).toFixed(2)}</p>
<button onClick={start}>开始</button>
<button onClick={stop}>停止</button>
<button onClick={reset}>重置</button>
</div>
);
}

保存上一次的值

function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}

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

return (
<div>
<p>现在: {count}, 之前: {prevCount}</p>
<button onClick={() => setCount(c => c + 1)}>增加</button>
</div>
);
}

useLayoutEffect

useLayoutEffectuseEffect 类似,但它在浏览器绘制之前同步执行。适合需要测量 DOM 或同步更新布局的场景。

function Tooltip({ text, children }) {
const [position, setPosition] = useState({ top: 0, left: 0 });
const tooltipRef = useRef();

useLayoutEffect(() => {
// 在绘制前测量和设置位置,避免闪烁
const rect = tooltipRef.current.getBoundingClientRect();
setPosition({
top: window.innerHeight / 2 - rect.height / 2,
left: window.innerWidth / 2 - rect.width / 2
});
}, [text]);

return (
<div ref={tooltipRef} style={{ position: 'absolute', ...position }}>
{text}
</div>
);
}

大多数情况下应使用 useEffect,除非需要同步更新 DOM。


性能优化 Hooks

useMemo

缓存计算结果,只有在依赖项改变时才重新计算。适用于计算成本高的场景。

function ExpensiveList({ items, filter }) {
// 只有 items 或 filter 变化时才重新过滤
const filteredItems = useMemo(() => {
console.log('过滤计算中...');
return items.filter(item => item.includes(filter));
}, [items, filter]);

return (
<ul>
{filteredItems.map(item => <li key={item}>{item}</li>)}
</ul>
);
}

不要过度使用 useMemo,简单计算的缓存可能比重新计算更慢。

useCallback

缓存函数定义,当函数作为依赖或传递给子组件时很有用。

function Parent() {
const [count, setCount] = useState(0);
const [items, setItems] = useState([]);

// ✅ 使用 useCallback 缓存,避免子组件不必要的重新渲染
const addItem = useCallback((item) => {
setItems(prev => [...prev, item]);
}, []); // 不依赖任何状态

return (
<div>
<button onClick={() => setCount(c => c + 1)}>计数: {count}</button>
<ItemList onAdd={addItem} />
</div>
);
}

// 使用 React.memo 配合 useCallback
const ItemList = React.memo(function ItemList({ onAdd }) {
console.log('ItemList 渲染');
return <button onClick={() => onAdd('item')}>添加</button>;
});

React 18/19 新 Hooks

useId (React 18)

生成唯一的 ID,通常用于无障碍辅助功能中的 aria 属性,确保服务端渲染和客户端渲染一致。

实时编辑器
function PasswordField() {
  const id = useId();
  return (
    <div style={{ padding: '10px' }}>
      <label htmlFor={id + '-password'}>密码:</label>
      <input id={id + '-password'} type="password" />
    </div>
  );
}
结果
Loading...

useTransition (React 18)

允许你将某些状态更新标记为"过渡",不会阻塞 UI 响应。适合处理可能导致界面卡顿的重渲染。

实时编辑器
function SearchResults() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  const handleSearch = (e) => {
    setQuery(e.target.value); // 紧急更新:立即更新输入框
    
    startTransition(() => {
      // 过渡更新:可以被中断
      const filtered = heavyFilter(e.target.value);
      setResults(filtered);
    });
  };

  return (
    <div>
      <input value={query} onChange={handleSearch} />
      {isPending && <span>搜索中...</span>}
      <ul>
        {results.map(r => <li key={r}>{r}</li>)}
      </ul>
    </div>
  );
}
结果
Loading...

useDeferredValue (React 18)

延迟更新某个值,让 UI 能先响应用户输入。

function SearchPage({ query }) {
const deferredQuery = useDeferredValue(query);
// query 立即更新,deferredQuery 延迟更新

return <Results query={deferredQuery} />;
}

useActionState (React 19)

useActionState 是 React 19 新增的 Hook,用于简化表单处理和数据变更操作。它可以自动管理 pending 状态、错误处理和返回值,特别适合配合 Server Actions 使用。

基本语法

const [state, formAction, isPending] = useActionState(fn, initialState, permalink?)

参数说明

  • fn:点击提交按钮时要调用的函数。函数接收两个参数:上一次的 state 和表单数据
  • initialState:state 的初始值
  • permalink(可选):包含此表单唯一 URL 的字符串,用于渐进增强

返回值

  • state:当前 state 值,初始为 initialState,之后是 action 返回的值
  • formAction:传递给 form 的 action 属性的函数
  • isPending:boolean,表示 action 是否正在执行

基础示例

function UpdateNameForm() {
const [error, submitAction, isPending] = useActionState(
async (previousState, formData) => {
const name = formData.get("name");

// 验证
if (!name || name.trim().length < 2) {
return "名字至少需要2个字符";
}

// 调用 API
const error = await updateName(name);
if (error) {
return error;
}

// 成功后跳转
redirect("/profile");
return null; // 成功时返回 null
},
null // 初始 state
);

return (
<form action={submitAction}>
<input type="text" name="name" required />
<button type="submit" disabled={isPending}>
{isPending ? "更新中..." : "更新名字"}
</button>
{error && <p className="error">{error}</p>}
</form>
);
}

配合 Server Actions 使用

// actions.js (Server Component)
"use server";

export async function updateUser(prevState, formData) {
const name = formData.get("name");

// 服务端验证
if (!name) {
return { error: "名字不能为空", success: false };
}

try {
await db.users.update({ name });
return { error: null, success: true };
} catch (err) {
return { error: "更新失败", success: false };
}
}

// UserForm.jsx (Client Component)
"use client";

import { useActionState } from "react";
import { updateUser } from "./actions";

export function UserForm() {
const [state, formAction, isPending] = useActionState(updateUser, {
error: null,
success: false
});

return (
<form action={formAction}>
<input name="name" type="text" />
<button disabled={isPending}>
{isPending ? "保存中..." : "保存"}
</button>
{state.error && <p className="error">{state.error}</p>}
{state.success && <p className="success">更新成功!</p>}
</form>
);
}

useFormStatus (React 19)

useFormStatus 是 React 19 新增的 Hook,用于在表单提交过程中获取状态信息。它必须在 <form> 内部的组件中使用,返回父级 <form> 的状态。

基本语法

const { pending, data, method, action } = useFormStatus();

返回值

  • pending:boolean,表单是否正在提交
  • data:FormData 对象,包含表单数据(提交时才有值)
  • method:字符串,表单提交方法("get" 或 "post")
  • action:表单的 action 属性值

使用场景useFormStatus 特别适合在设计系统中使用,可以让按钮组件感知表单状态而不需要 props 传递。

示例:禁用提交按钮

import { useFormStatus } from "react-dom";

// 设计系统按钮组件
function SubmitButton({ children, className }) {
const { pending } = useFormStatus();

return (
<button
type="submit"
disabled={pending}
className={className}
>
{pending ? "提交中..." : children}
</button>
);
}

// 使用
function ContactForm() {
async function handleSubmit(formData) {
await sendMessage(formData.get("message"));
}

return (
<form action={handleSubmit}>
<textarea name="message" required />
<SubmitButton>发送消息</SubmitButton>
</form>
);
}

示例:显示提交数据

function FormStatusDisplay() {
const { pending, data, method } = useFormStatus();

if (!pending) return null;

return (
<div className="status-bar">
正在{method === "post" ? "提交" : "获取"}数据...
{data && (
<span>{data.get("name")}</span>
)}
</div>
);
}

重要说明

  • useFormStatus 必须在 <form> 内部的组件中调用,否则返回的 pending 始终为 false
  • 它实际上是读取父级 <form> 提供的 Context,所以需要作为子组件使用

useOptimistic (React 19)

useOptimistic 用于实现乐观更新——在异步操作完成前就显示预期结果,提升用户体验。当操作失败时,会自动回滚到原始状态。

基本语法

const [optimisticState, addOptimistic] = useOptimistic(actualState, updateFn);

参数说明

  • actualState:真实的状态值
  • updateFn:计算乐观状态的函数,接收 (currentState, optimisticValue) 并返回新的乐观状态

返回值

  • optimisticState:乐观状态(操作进行中时显示乐观值,否则显示真实值)
  • addOptimistic:触发乐观更新的函数

示例:消息发送

import { useOptimistic } from "react";

function MessageThread({ messages, sendMessage }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => [
...state,
{
id: "temp-" + Date.now(),
text: newMessage,
sending: true // 标记为发送中
}
]
);

async function handleSubmit(formData) {
const text = formData.get("message");

// 立即显示乐观消息
addOptimisticMessage(text);

// 发送到服务器
await sendMessage(text);
// 成功后,messages 会更新,乐观消息被替换
}

return (
<div>
<div className="messages">
{optimisticMessages.map((msg) => (
<div
key={msg.id}
className={msg.sending ? "message sending" : "message"}
>
{msg.text}
{msg.sending && <span className="spinner">...</span>}
</div>
))}
</div>

<form action={handleSubmit}>
<input name="message" placeholder="输入消息..." required />
<button type="submit">发送</button>
</form>
</div>
);
}

示例:点赞功能

function LikeButton({ postId, initialLikes, isLiked }) {
const [likes, setLikes] = useState(initialLikes);
const [liked, setLiked] = useState(isLiked);

const [optimisticLikes, addLike] = useOptimistic(
likes,
(currentLikes, increment) => currentLikes + increment
);

async function handleLike() {
// 乐观更新:立即显示点赞效果
const increment = liked ? -1 : 1;
addLike(increment);

try {
await toggleLike(postId);
setLiked(!liked);
setLikes(prev => prev + increment);
} catch (error) {
// 失败时自动回滚到 optimisticLikes 之前的值
console.error("点赞失败", error);
}
}

return (
<button onClick={handleLike} className={liked ? "liked" : ""}>
❤️ {optimisticLikes}
</button>
);
}

use API (React 19)

use 是 React 19 引入的新 API,用于在渲染过程中读取 Promise 或 Context 的值。与 Hooks 不同,use 可以在条件语句和循环中调用。

基本语法

import { use } from "react";

const value = use(resource);

参数

  • resource:要读取的资源,可以是 Promise 或 Context

返回值

  • Promise 解析后的值,或 Context 的当前值

读取 Promise

use 可以直接读取 Promise,并与 Suspense 配合使用:

import { use, Suspense } from "react";

// 获取数据的函数返回 Promise
function fetchUser(id) {
return fetch(`/api/users/${id}`).then(res => res.json());
}

function UserProfile({ userPromise }) {
// use 会"挂起"组件直到 Promise 解析完成
const user = use(userPromise);

return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}

function UserPage({ userId }) {
const userPromise = fetchUser(userId); // 在父组件创建 Promise

return (
<Suspense fallback={<div>加载中...</div>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}

错误处理

当 Promise 被 reject 时,可以使用 Error Boundary 捕获:

import { ErrorBoundary } from "react-error-boundary";

function UserPage({ userId }) {
return (
<ErrorBoundary fallback={<div>加载失败</div>}>
<Suspense fallback={<div>加载中...</div>}>
<UserProfile userPromise={fetchUser(userId)} />
</Suspense>
</ErrorBoundary>
);
}

读取 Context

use 也可以读取 Context,与 useContext 的区别是 use 可以在条件语句中调用:

import { use, createContext } from "react";

const ThemeContext = createContext("light");

function ThemedButton({ show }) {
// useContext 不允许在条件语句中调用
// 但 use 可以!
if (show) {
const theme = use(ThemeContext);
return <button className={theme}>主题按钮</button>;
}
return null;
}

与 useContext 的对比

特性useContextuse
调用位置必须在顶层可以在条件语句、循环中
类型HookAPI(不是 Hook)
推荐使用-✅ 官方推荐优先使用 use

服务器组件与客户端组件协作

use 特别适合在服务端获取数据、客户端消费的场景:

// page.jsx (Server Component)
import { fetchComments } from "./api";
import { Comments } from "./Comments";

export default function Page() {
// 服务端创建 Promise
const commentsPromise = fetchComments();

// 传递给客户端组件
return <Comments commentsPromise={commentsPromise} />;
}

// Comments.jsx (Client Component)
"use client";

import { use, Suspense } from "react";

export function Comments({ commentsPromise }) {
// 客户端使用 use 读取
const comments = use(commentsPromise);

return (
<ul>
{comments.map(c => (
<li key={c.id}>{c.text}</li>
))}
</ul>
);
}

注意事项

  • use 必须在组件或 Hook 中调用
  • 服务器组件中数据获取应优先使用 async/await
  • 客户端组件中创建的 Promise 每次渲染都会重新创建,应从服务端传递

自定义 Hook

自定义 Hook 是将逻辑提取到可重用函数中的方式。必须以 use 开头,这样 React 能检查它是否遵循 Hook 规则。

窗口大小 Hook

实时编辑器
function useWindowSize() {
  const [size, setSize] = useState({ width: 0, height: 0 });

  useEffect(() => {
    const handleResize = () => setSize({ 
      width: window.innerWidth, 
      height: window.innerHeight 
    });
    window.addEventListener('resize', handleResize);
    handleResize();
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size;
}

function DisplaySize() {
  const { width, height } = useWindowSize();
  return (
    <div style={{ padding: '10px', background: '#f0fdf4', borderRadius: '4px' }}>
      <p style={{ margin: 0 }}>窗口大小: <strong>{width} x {height}</strong></p>
      <small style={{ color: '#666' }}>尝试调整浏览器窗口大小看看</small>
    </div>
  );
}

render(<DisplaySize />);
结果
Loading...

本地存储 Hook

function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});

const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};

return [storedValue, setValue];
}

// 使用
function App() {
const [name, setName] = useLocalStorage('name', '张三');
return <input value={name} onChange={e => setName(e.target.value)} />;
}

防抖 Hook

function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);

useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);

return () => clearTimeout(handler);
}, [value, delay]);

return debouncedValue;
}

// 使用:搜索框防抖
function SearchInput() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);

useEffect(() => {
if (debouncedSearchTerm) {
// 执行搜索
fetchResults(debouncedSearchTerm);
}
}, [debouncedSearchTerm]);

return <input value={searchTerm} onChange={e => setSearchTerm(e.target.value)} />;
}

Hooks 速查表

Hook用途常见场景
useState管理状态表单输入、切换开关、计数器
useEffect副作用数据获取、订阅、DOM 操作
useReducer复杂状态表单状态、复杂状态转换
useContext全局数据主题、用户信息、语言设置
useRefDOM 引用/可变值焦点管理、定时器 ID、前值
useMemo缓存计算列表过滤、复杂计算
useCallback缓存函数传递给优化的子组件
useLayoutEffect同步副作用DOM 测量、布局更新
useId唯一 ID表单关联、无障碍属性
useTransition非紧急更新搜索结果、列表过滤
useDeferredValue延迟值搜索防抖
useActionState (19)表单动作状态表单提交、数据变更
useFormStatus (19)表单提交状态提交按钮禁用、加载指示
useOptimistic (19)乐观更新点赞、消息发送
use (19)读取资源Promise、Context

小结

  1. useState:管理本地状态,支持函数式更新和惰性初始化。
  2. useEffect:处理副作用,通过依赖数组控制执行时机。
  3. useReducer:管理复杂状态逻辑,适合状态有多个子值的场景。
  4. useContext:避免属性钻取,共享全局数据。
  5. useRef:获取 DOM 元素,存储不触发渲染的可变值。
  6. useMemo/useCallback:性能优化,缓存计算结果/函数。
  7. useId/useTransition:React 18 新增功能。
  8. useActionState/useOptimistic:React 19 新增功能。

练习

  1. 使用 useRef 实现一个"点击外部关闭弹窗"的功能。
  2. 使用 useMemo 优化一个经过大量数据过滤的列表组件。
  3. 创建一个自定义 Hook useLocalStorage 来同步状态到本地存储。
  4. 使用 useReducer 实现一个购物车功能(添加、删除、修改数量)。
  5. 创建一个 useFetch 自定义 Hook,处理加载状态和错误。

参考资源