React 自定义 Hooks 完全指南:逻辑复用与模块化

小飞兽 React 319 次阅读 2026-07-10

React 自定义 Hooks 完全指南:逻辑复用与模块化

一、Introduction

React 内置的 Hooks(useState、useEffect、useRef 等)提供了构建 UI 的基础能力,但当你的组件中有复杂的逻辑需要在多个地方复用时,内置 Hooks 就不够用了。自定义 Hook(Custom Hook)应运而生:它本质上就是一个使用其他 Hooks 的 JavaScript 函数,通过约定以 use 开头来让 React 自动执行 lint 规则检查。

自定义 Hook 是 React 中最重要的代码复用模式,它比 HOC(高阶组件)和 Render Props 更直观、更灵活,也更符合 React Hooks 的设计哲学。学会写自定义 Hook 是 React 开发者从"会用"到"用好"的分水岭。

---

二、自定义 Hook 的核心概念

#### 2.1 自定义 Hook 本质

自定义 Hook 就是带 use 前缀的函数,内部可以调用其他 Hooks,React 通过函数名是否以 use 开头来判断是否应用 Hooks 规则。

// 自定义 Hook:提取计数器逻辑
import { useState, useCallback } from 'react';

// 这个函数就是一个自定义 Hook
function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);

  const increment = useCallback(() => setCount(c => c + 1), []);
  const decrement = useCallback(() => setCount(c => Math.max(0, c - 1)), []);
  const reset = useCallback(() => setCount(initialValue), [initialValue]);

  // 返回状态和操作方法
  return { count, increment, decrement, reset };
}

// 使用自定义 Hook 的组件
function CounterA() {
  const { count, increment, decrement, reset } = useCounter(0);
  return (
    <div>
      <p>A 计数器:{count}</p>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
      <button onClick={reset}>重置</button>
    </div>
  );
}

function CounterB() {
  // 完全独立的计数器实例,逻辑复用但状态隔离
  const { count, increment, reset } = useCounter(10);
  return (
    <div>
      <p>B 计数器(初始10):{count}</p>
      <button onClick={increment}>+1</button>
      <button onClick={reset}>重置</button>
    </div>
  );
}

---

三、实用自定义 Hook 实战

#### 3.1 useLocalStorage:持久化状态到浏览器本地存储

import { useState, useEffect, useCallback } from 'react';

// 自定义 Hook:将 state 同步到 localStorage
function useLocalStorage(key, initialValue) {
  // 初始化:优先从 localStorage 读取,否则用初始值
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.warn(`读取 localStorage key "${key}" 失败:`, error);
      return initialValue;
    }
  });

  // 每当 storedValue 变化时,同步到 localStorage
  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(storedValue));
    } catch (error) {
      console.warn(`写入 localStorage key "${key}" 失败:`, error);
    }
  }, [key, storedValue]);

  // 返回稳定的 setter 函数
  const setValue = useCallback((value) => {
    setStoredValue(prev => {
      // 支持直接传入值或函数
      const valueToStore = value instanceof Function ? value(prev) : value;
      return valueToStore;
    });
  }, []);

  return [storedValue, setValue];
}

// 使用示例
function SettingsPanel() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  const [username, setUsername] = useLocalStorage('username', '');

  return (
    <div style={{ background: theme === 'dark' ? '#333' : '#fff', color: theme === 'dark' ? '#fff' : '#333', padding: 16 }}>
      <h3>设置面板(刷新后设置不丢失)</h3>
      <div>
        <label>
          主题:
          <select value={theme} onChange={e => setTheme(e.target.value)}>
            <option value="light">浅色</option>
            <option value="dark">深色</option>
          </select>
        </label>
      </div>
      <div style={{ marginTop: 12 }}>
        <label>
          用户名:
          <input
            value={username}
            onChange={e => setUsername(e.target.value)}
            placeholder="输入用户名"
          />
        </label>
      </div>
      <p style={{ fontSize: 12, marginTop: 12 }}>
        当前主题:{theme},用户名:{username || '(未设置)'}
      </p>
    </div>
  );
}

export default SettingsPanel;

#### 3.2 useDebounce:防抖 Hook

import { useState, useEffect } from 'react';

// 延迟更新值的 Hook,常用于搜索框输入防抖
function useDebounce(value, delay = 500) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    // 每次 value 变化时,重置定时器
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    // 清理函数:在下次 effect 执行时清除上次的定时器
    // 这样只有 value 停止变化 delay 毫秒后,debouncedValue 才更新
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// 使用示例:搜索框防抖
function DebouncedSearch() {
  const [query, setQuery] = useState('');
  // 只有用户停止输入 500ms 后,debouncedQuery 才会更新
  const debouncedQuery = useDebounce(query, 500);

  useEffect(() => {
    if (!debouncedQuery) return;
    console.log(`发起 API 请求,搜索词:${debouncedQuery}`);
    // 这里放实际的 API 调用,如 fetch(`/api/search?q=${debouncedQuery}`)
  }, [debouncedQuery]);

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="输入搜索词(500ms 防抖)..."
        style={{ padding: 8, width: 300 }}
      />
      <p style={{ color: '#888', marginTop: 8 }}>
        搜索词(实时):{query}
        <br />
        防抖后值:{debouncedQuery || '(等待输入...)'}
      </p>
    </div>
  );
}

export { useDebounce, DebouncedSearch };

#### 3.3 useFetch:数据请求 Hook

import { useState, useEffect } from 'react';

// 封装异步数据请求逻辑
function useFetch(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    const fetchData = async () => {
      setLoading(true);
      setError(null);

      try {
        const response = await fetch(url, {
          ...options,
          signal: controller.signal,
        });

        if (!response.ok) {
          throw new Error(`HTTP 错误: ${response.status} ${response.statusText}`);
        }

        const json = await response.json();
        setData(json);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        // 只有在没有取消请求时才设置 loading 为 false
        setLoading(false);
      }
    };

    fetchData();

    // 清理:组件卸载或 url 变化时取消请求
    return () => controller.abort();
  }, [url]);

  return { data, loading, error };
}

// 使用示例
function GitHubUser({ username }) {
  const { data, loading, error } = useFetch(
    username ? `https://api.github.com/users/${username}` : null
  );

  if (!username) return <p>请输入 GitHub 用户名</p>;
  if (loading) return <p>加载中...</p>;
  if (error) return <p>错误:{error}</p>;

  return (
    <div>
      <img src={data.avatar_url} alt={data.login} style={{ width: 80, borderRadius: '50%' }} />
      <h3>{data.name || data.login}</h3>
      <p>粉丝:{data.followers},关注:{data.following}</p>
      <p>位置:{data.location || '未知'}</p>
    </div>
  );
}

export { useFetch, GitHubUser };

---

四、运行效果说明

useCounter:CounterA 和 CounterB 是完全独立的计数器实例,共享同一套增减重置逻辑,但各自维护自己的状态,互不影响。刷新页面计数器会重置为初始值。

useLocalStorage:SettingsPanel 的设置写入 localStorage,刷新页面或关闭浏览器后重新打开,theme 和 username 保持上次的值。

useDebounce:在搜索框快速输入"react hooks",debouncedQuery 只在停止输入 500ms 后才更新,API 请求次数大大减少。

useFetch:输入 GitHub 用户名(如 "facebook"),自动发起请求并展示用户头像、粉丝数和位置。

---

五、常见问题(FAQ)

#### Q1:自定义 Hook 和普通函数有什么区别?

关键区别:自定义 Hook 可以调用其他 Hooks,普通函数不行。React 根据函数名是否以 use 开头来判断它是否使用了 Hooks 规则(这也是为什么自定义 Hook 必须以 use 开头)。此外,自定义 Hook 的每次调用都拥有完全独立的状态——这叫"状态共享隔离",每次组件调用 Hook,React 内部都会为其分配独立的状态存储。

// 两个组件各调用一次 useCustomHook,它们的状态完全独立
function ComponentA() {
  const { value } = useCustomHook(); // 独立的状态 "value"
}
function ComponentB() {
  const { value } = useCustomHook(); // 另一个独立的状态 "value"
}

#### Q2:自定义 Hook 可以使用其他自定义 Hook 吗?

可以。 自定义 Hook 之间可以自由嵌套和组合,形成逻辑的层层抽象:

function useAuth() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  // ... 认证逻辑
  return { user, loading, login, logout };
}

function useUserData() {
  const { user } = useAuth(); // 在自定义 Hook 中调用另一个自定义 Hook
  const [data, setData] = useState(null);

  useEffect(() => {
    if (!user) return;
    fetchUserData(user.id).then(setData);
  }, [user]);

  return { data, loading: !user && !data };
}

#### Q3:自定义 Hook 的逻辑和组件的渲染逻辑应该怎么分离?

原则:自定义 Hook 只负责数据和逻辑,组件只负责 UI 渲染。不要在自定义 Hook 里直接返回 JSX。

// 错误:自定义 Hook 返回 JSX
function useLoginForm() {
  // ... 表单逻辑
  return <form>...</form>; // 不要这样做
}

// 正确:自定义 Hook 只返回数据和方法
function useLoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      await login(email, password);
    } catch (err) {
      setError(err.message);
    }
  };

  return { email, setEmail, password, setPassword, error, handleSubmit };
}

function LoginForm() {
  // 组件只负责 UI,数据和逻辑来自自定义 Hook
  const { email, setEmail, password, setPassword, error, handleSubmit } = useLoginForm();
  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={e => setEmail(e.target.value)} />
      <input type="password" value={password} onChange={e => setPassword(e.target.value)} />
      {error && <p>{error}</p>}
      <button type="submit">登录</button>
    </form>
  );
}

---

六、延伸阅读

  • [React Hooks 完全指南:useState 与 useEffect 核心用法]()
  • [React useRef 完全指南:操作 DOM 与持久化变量]()
  • [React Context API 状态管理深度理解]()
  • [React 组件设计模式:高阶组件与 Render Props]()

---