React Hooks 完全指南:从内置 Hook 到自定义 Hook,一文彻底掌握

React Hooks 完全指南:从内置 Hook 到自定义 Hook,一文彻底掌握

React 16.8 引入了 Hooks,从此函数组件拥有了类组件的一切能力。本文将带你系统学习所有内置 Hooks 的用法,并教你如何封装属于自己的自定义 Hook,让代码复用变得轻而易举。


前言:为什么需要 Hooks?

在 React 16.8 之前,函数组件只能接收 props 并渲染 UI,无法管理状态、无法处理副作用。如果你需要这些能力,就必须写成类组件。

Hooks 的出现彻底改变了这一局面。它让开发者在不编写类组件的情况下使用 React 的状态和其他特性,极大地扩展了函数组件的能力。从此,函数式编程范式在 React 中得到了更广泛的应用。

Hooks 的核心价值

  • 逻辑复用:将组件逻辑抽离成可复用的函数,而不仅仅是 UI 复用
  • 代码清晰:将复杂组件拆分成多个小函数,每个函数专注一件事
  • 性能优化 :通过 useMemouseCallback 等 Hook 避免不必要的计算和渲染

一、Hooks 的黄金规则

Hooks 看起来是普通的 JavaScript 函数,但它们代表了一种特殊类型的可复用 UI 逻辑,对调用位置有着严格限制。

规则一:只在顶层调用 Hooks

不要 在循环、条件判断、嵌套函数或 try/catch/finally 块中调用 Hooks。始终 在 React 函数组件的顶层、任何 return 之前调用 Hooks。

jsx 复制代码
// ❌ 错误:在条件语句中调用
function Bad({ cond }) {
  if (cond) {
    const theme = useContext(ThemeContext); // 禁止!
  }
}

// ❌ 错误:在循环中调用
function Bad() {
  for (let i = 0; i < 10; i++) {
    const theme = useContext(ThemeContext); // 禁止!
  }
}

// ❌ 错误:在条件 return 之后调用
function Bad({ cond }) {
  if (cond) return;
  const theme = useContext(ThemeContext); // 禁止!
}

// ❌ 错误:在事件处理函数中调用
function Bad() {
  function handleClick() {
    const theme = useContext(ThemeContext); // 禁止!
  }
}

// ❌ 错误:在类组件中调用
class Bad extends React.Component {
  render() {
    useEffect(() => {}); // 禁止!
  }
}

// ✅ 正确:在函数组件顶层调用
function Good() {
  const [count, setCount] = useState(0); // ✅
  const theme = useContext(ThemeContext); // ✅
  // ...
}

为什么? React 依赖 Hooks 的调用顺序来正确保存每个 Hook 的状态。如果 Hooks 被条件性地调用,顺序就会被打乱,导致状态错乱。

规则二:只在 React 函数中调用 Hooks

  • ✅ 在 React 函数组件中调用
  • ✅ 在自定义 Hook 中调用
  • ❌ 不要在普通 JavaScript 函数中调用
jsx 复制代码
// ❌ 错误:在普通 JS 函数中调用
function normalFunction() {
  const [count, setCount] = useState(0); // 禁止!
}

// ✅ 正确:在自定义 Hook 中调用
function useCustomHook() {
  const [count, setCount] = useState(0); // ✅ 自定义 Hook 可以调用其他 Hook
  return count;
}

补充:自定义 Hook 可以调用其他 Hook,这正是它们存在的全部意义。因为自定义 Hook 也只在函数组件渲染时被调用。


二、内置 Hooks 分类全景

React 内置了十余个 Hooks,按功能可分为以下几大类:

分类 Hooks 用途
State Hooks useStateuseReducer 管理组件状态
Context Hooks useContext 读取订阅上下文
Ref Hooks useRefuseImperativeHandle 保存不用于渲染的信息
Effect Hooks useEffectuseLayoutEffectuseInsertionEffect 连接外部系统
Performance Hooks useMemouseCallback 跳过计算和不必要的重渲染
Other Hooks useTransitionuseDeferredValueuseIduseSyncExternalStoreuseDebugValue 并发渲染、调试等特殊场景

三、State Hooks:状态管理

1. useState ------ 最基础的状态 Hook

useState 允许在函数组件中添加状态。它返回一个状态值和更新该状态值的函数。

jsx 复制代码
import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0); // 初始值为 0

  return (
    <div>
      <p>点击了 {count} 次</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

惰性初始化:如果初始状态需要复杂计算,可以传入函数:

jsx 复制代码
const [state, setState] = useState(() => {
  const expensiveResult = someExpensiveComputation();
  return expensiveResult;
});

2. useReducer ------ 复杂状态逻辑

当状态更新逻辑复杂、涉及多个子值或依赖先前状态时,useReducer 是比 useState 更好的选择。

jsx 复制代码
import { useReducer } from 'react';

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </>
  );
}

四、Context Hook:跨层级传递数据

useContext ------ 摆脱 props 层层传递

useContext 让组件从祖先组件接收信息,而无需将其作为 props 逐层传递。

jsx 复制代码
import { useContext, createContext } from 'react';

const ThemeContext = createContext('light');

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  return <Button />;
}

function Button() {
  const theme = useContext(ThemeContext); // 直接读取,无需 props 传递
  return <button className={theme}>按钮</button>;
}

五、Ref Hooks:保存不用于渲染的数据

1. useRef ------ 不触发渲染的"存储箱"

Ref 允许组件保存一些不用于渲染的信息,比如 DOM 节点或 timeout ID。与 state 不同,更新 ref 不会触发组件重新渲染

jsx 复制代码
import { useRef, useEffect } from 'react';

function TextInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus(); // 组件挂载后自动聚焦
  }, []);

  return <input ref={inputRef} type="text" />;
}

2. useImperativeHandle ------ 自定义暴露给父组件的 ref

useImperativeHandle 允许你在使用 ref 时自定义暴露给父组件的实例值或方法。这在你希望限制父组件对子组件的访问权限时非常有用。

jsx 复制代码
import { forwardRef, useImperativeHandle, useRef } from 'react';

const ChildInput = forwardRef((props, ref) => {
  const inputRef = useRef(null);

  // 只暴露 focus 方法给父组件,不暴露整个 DOM 节点
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    clear: () => {
      inputRef.current.value = '';
    }
  }));

  return <input ref={inputRef} />;
});

function Parent() {
  const childRef = useRef(null);
  return (
    <>
      <ChildInput ref={childRef} />
      <button onClick={() => childRef.current.focus()}>聚焦子输入框</button>
    </>
  );
}

六、Effect Hooks:连接外部系统

1. useEffect ------ 最常用的副作用 Hook

useEffect 允许组件连接到外部系统并与之同步。这包括数据获取、订阅管理、DOM 操作等。

jsx 复制代码
import { useState, useEffect } from 'react';

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    // 副作用:连接聊天室
    const connection = createConnection(roomId);
    connection.connect();
    connection.on('message', (msg) => {
      setMessages(prev => [...prev, msg]);
    });

    // 清理函数:组件卸载或 roomId 变化时断开连接
    return () => {
      connection.disconnect();
    };
  }, [roomId]); // 依赖数组:roomId 变化时重新执行

  return <div>{/* 渲染消息列表 */}</div>;
}

依赖数组的三种情况

  • useEffect(() => {}, []) --- 只在组件挂载时执行一次
  • useEffect(() => {}, [a, b]) --- 在 a 或 b 变化时执行
  • useEffect(() => {}) --- 每次渲染后都执行(慎用!)

重要提醒 :Effect 是 React 范式中的"脱围机制"。如果不需要与外部系统交互,那么你可能不需要 Effect

2. useLayoutEffect ------ 在浏览器绘制前同步执行

useLayoutEffectuseEffect 类似,但它在浏览器重新绘制屏幕之前同步执行。适合测量布局、防止闪烁等场景。

jsx 复制代码
import { useLayoutEffect, useRef, useState } from 'react';

function Tooltip() {
  const ref = useRef(null);
  const [height, setHeight] = useState(0);

  // 在浏览器绘制前测量 DOM 高度,避免闪烁
  useLayoutEffect(() => {
    if (ref.current) {
      setHeight(ref.current.getBoundingClientRect().height);
    }
  }, []);

  return <div ref={ref}>高度是:{height}px</div>;
}

useEffect vs useLayoutEffectuseEffect 是异步执行的,适合绝大多数场景;useLayoutEffect 是同步执行的,适合需要在布局更新后立即执行的操作。


七、Performance Hooks:性能优化

1. useMemo ------ 缓存计算结果

useMemo 返回一个记忆化的值,避免在每次渲染时重复计算。

jsx 复制代码
import { useMemo, useState } from 'react';

function ExpensiveComponent({ a, b }) {
  const [count, setCount] = useState(0);

  // 只有 a 或 b 变化时才重新计算,否则直接使用缓存值
  const expensiveResult = useMemo(() => {
    console.log('执行昂贵计算...');
    return a * b * 1000; // 模拟复杂计算
  }, [a, b]);

  return (
    <div>
      结果:{expensiveResult}
      <button onClick={() => setCount(count + 1)}>无关状态:{count}</button>
    </div>
  );
}

2. useCallback ------ 缓存函数引用

useCallback 返回一个记忆化的回调函数,防止不必要的渲染。

jsx 复制代码
import { useCallback, useState } from 'react';

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

  // 只有依赖变化时才创建新函数,否则复用之前的
  const handleClick = useCallback(() => {
    console.log('点击了');
  }, []); // 空依赖 → 永远不变

  return (
    <>
      <Child onClick={handleClick} />
      <button onClick={() => setCount(count + 1)}>父组件状态:{count}</button>
    </>
  );
}

useMemo vs useCallbackuseMemo 缓存useCallback 缓存函数。两者都用于性能优化,但不要过度使用------只有在确实存在性能问题时才使用。


八、React 18 新增 Hooks

React 18 引入了多个新 Hook,主要围绕并发渲染服务端渲染两大主题。

1. useTransition ------ 标记非紧急更新

useTransition 让你将某些状态更新标记为不紧急。默认情况下其他状态更新被视为紧急的,React 允许紧急更新(如输入框输入)中断非紧急更新(如渲染搜索结果列表)。

jsx 复制代码
import { useState, useTransition } from 'react';

function SearchPage() {
  const [input, setInput] = useState('');
  const [list, setList] = useState([]);
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    const value = e.target.value;
    setInput(value); // 紧急更新:立即更新输入框

    startTransition(() => {
      // 非紧急更新:可以被打断
      const newList = generateLargeList(value);
      setList(newList);
    });
  };

  return (
    <div>
      <input value={input} onChange={handleChange} />
      {isPending && <span>加载中...</span>}
      <ul>{list.map(item => <li key={item.id}>{item.name}</li>)}</ul>
    </div>
  );
}

2. useDeferredValue ------ 延迟非紧急值

useDeferredValue 让你推迟重新渲染树的非紧急部分。它类似于防抖,但没有固定的时间延迟------React 会在第一次渲染完成后立即尝试延迟渲染。

jsx 复制代码
import { useState, useDeferredValue, useMemo } from 'react';

function SearchPage() {
  const [text, setText] = useState('');
  const deferredText = useDeferredValue(text);

  // 只有 deferredText 变化时才重新计算,且计算可被中断
  const list = useMemo(() => {
    return generateLargeList(deferredText);
  }, [deferredText]);

  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <List items={list} />
    </div>
  );
}

3. useId ------ 生成唯一 ID

useId 用于在客户端和服务器上生成唯一 ID,同时避免水合不匹配。它主要用于与需要唯一 ID 的可访问性 API 集成的组件库。

jsx 复制代码
import { useId } from 'react';

function FormField({ label }) {
  const id = useId(); // 每次调用生成唯一 ID
  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input id={id} type="text" />
    </div>
  );
}

九、自定义 Hook:封装可复用逻辑

自定义 Hook 允许你将组件逻辑提取到可复用的函数中。它是 React 实现逻辑复用的核心机制。

核心规则

  1. Hook 名称必须以 use 开头,然后紧跟大写字母
  2. 自定义 Hook 可以调用其他 Hook(内置 Hook 或其他自定义 Hook)
  3. 自定义 Hook 应该只在函数组件渲染时被调用

示例一:useOnlineStatus ------ 监听网络状态

假设你需要在多个组件中追踪用户是否在线:

jsx 复制代码
// useOnlineStatus.js
import { useState, useEffect } from 'react';

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);

  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return isOnline;
}

// 使用
function StatusBar() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? '✅ 在线' : '❌ 离线'}</h1>;
}

function SaveButton() {
  const isOnline = useOnlineStatus();
  return (
    <button disabled={!isOnline}>
      {isOnline ? '保存' : '重连中...'}
    </button>
  );
}

示例二:useLocalStorage ------ 与 localStorage 同步

jsx 复制代码
// useLocalStorage.js
import { useState } from 'react';

function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error('读取 localStorage 失败:', 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('写入 localStorage 失败:', error);
    }
  };

  return [storedValue, setValue];
}

// 使用
function UserPreferences() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  const [language, setLanguage] = useLocalStorage('language', 'zh');

  return (
    <div>
      <select value={theme} onChange={(e) => setTheme(e.target.value)}>
        <option value="light">亮色</option>
        <option value="dark">暗色</option>
      </select>
      <select value={language} onChange={(e) => setLanguage(e.target.value)}>
        <option value="zh">中文</option>
        <option value="en">英文</option>
      </select>
    </div>
  );
}

示例三:useDebounce ------ 防抖

jsx 复制代码
// useDebounce.js
import { useState, useEffect } from 'react';

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

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

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

  return debouncedValue;
}

// 使用
function SearchInput() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearch = useDebounce(searchTerm, 500);

  useEffect(() => {
    if (debouncedSearch) {
      // 只有用户停止输入 500ms 后才会发起搜索请求
      fetchSearchResults(debouncedSearch);
    }
  }, [debouncedSearch]);

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

自定义 Hook 的最佳实践

  1. 保持聚焦:每个自定义 Hook 应该有单一、具体的用途
  2. 避免不必要的依赖:注意依赖数组,防止无限循环
  3. 让调用代码更声明式:好的自定义 Hook 通过约束其行为让调用代码更清晰
  4. 返回你需要的数据:Hook 可以返回任何值------数组、对象、单个值或函数
  5. 按领域或功能组织:将相关的 Hook 放在一起

十、完整 Hook 速查表

Hook 分类 用途 关键点
useState State 管理简单状态 返回 [state, setState]
useReducer State 管理复杂状态逻辑 适合多个子值或复杂更新
useContext Context 读取上下文 替代 props 层层传递
useRef Ref 保存不触发渲染的值 .current 属性可变
useImperativeHandle Ref 自定义暴露的 ref 配合 forwardRef 使用
useEffect Effect 处理副作用 依赖数组控制执行时机
useLayoutEffect Effect 同步执行副作用 在浏览器绘制前执行
useMemo Performance 缓存计算结果 避免重复计算
useCallback Performance 缓存函数引用 避免子组件不必要的重渲染
useTransition Other 标记非紧急更新 React 18 新特性
useDeferredValue Other 延迟非紧急值 React 18 新特性
useId Other 生成唯一 ID React 18 新特性
useDebugValue Other 调试自定义 Hook 仅在 React DevTools 中显示

结语

React Hooks 是 React 生态中最重要的革新之一。从 useStateuseEffect,从 useMemo 到自定义 Hook,这套 API 设计让函数组件变得无比强大,同时保持了代码的简洁和可维护性。

记住三条核心原则

  1. 只在顶层调用 Hooks------不要放在循环、条件或嵌套函数中
  2. 只在 React 函数中调用 Hooks------函数组件或自定义 Hook
  3. 自定义 Hook 必须以 use 开头

当你发现自己在多个组件中重复相同的逻辑时,就是时候提取一个自定义 Hook了。这正是 Hooks 设计的初衷------让逻辑复用变得简单而优雅。

如果觉得有用,欢迎点赞、收藏、转发,让更多小伙伴受益!我们下期再见 👋

相关推荐
为你学会写情书33 分钟前
React Router 完全指南:从入门到实战,一文搞懂路由核心技巧
react.js
xm_xm_xm_112 小时前
react17版本以前类组件常用操作
前端·javascript·react.js
Damai1 天前
Next.js 切换动态路由后,为什么页面还保留着上一个分类的数据?
前端·react.js
用户298698530141 天前
React 中 Word 文档转图片的实践方案
前端·javascript·react.js
专业抄代码选手1 天前
06|双缓冲与 Commit:让 Fiber 支持第二次渲染
前端·javascript·react.js
专业抄代码选手1 天前
05|把递归渲染拆成 Fiber:让一棵大树可以暂停
前端·javascript·react.js
墨白曦煜1 天前
智能体架构范式总结(React、Plan-And-Solve、Reflection)
前端·react.js·前端框架
zzzzzz3101 天前
react-bits:把动效组件库当成前端表达的参考系
react.js·开源·动效
Shinner欣儿2 天前
React18 和 19 新特性结合看
前端·react.js