React Hooks 自定义封装最佳实践:从抽象到可维护的实战指南

By | 2026年7月4日

前言

React Hooks 自 React 16.8 引入以来,彻底改变了组件逻辑复用的方式。自定义 Hook 是 Hooks 最强大的特性之一,它允许我们将组件逻辑提取到可复用的函数中。然而,实际开发中,很多开发者容易陷入“过度抽象”或“设计不当”的陷阱,导致 Hook 难以维护、复用性差。本文将分享我在实际项目中积累的自定义 Hook 封装最佳实践,通过具体案例带你掌握从设计到测试的完整流程。

一、设计原则:单一职责与组合优于继承

1.1 单一职责

一个 Hook 只做一件事。例如,不要将数据获取和表单验证混在一个 Hook 中。


// ❌ 错误:一个 Hook 做两件事
function useUserAndValidate() {
  const [user, setUser] = useState(null);
  const [errors, setErrors] = useState({});
  // ... 数据获取与验证逻辑混在一起
}

// ✅ 正确:分离关注点
function useUser(userId) {
  const [user, setUser] = useState(null);
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);
  return user;
}

function useValidation(values) {
  const [errors, setErrors] = useState({});
  useEffect(() => {
    // 仅验证逻辑
  }, [values]);
  return errors;
}

1.2 组合优于继承

利用多个基础 Hook 组合成复杂 Hook,而不是在一个 Hook 中堆砌所有功能。


function useUserProfile(userId) {
  const user = useUser(userId);
  const permissions = usePermissions(user?.role);
  const theme = useTheme();
  return { user, permissions, theme };
}

二、状态管理:避免“状态爆炸”

2.1 使用 useReducer 替代多个 useState

当状态逻辑复杂且包含多个子值时,使用 useReducer 让状态更新更可预测。


function useForm(initialValues) {
  const [state, dispatch] = useReducer((state, action) => {
    switch (action.type) {
      case 'SET_FIELD':
        return { ...state, [action.field]: action.value };
      case 'RESET':
        return initialValues;
      default:
        return state;
    }
  }, initialValues);

  const setField = useCallback((field, value) => {
    dispatch({ type: 'SET_FIELD', field, value });
  }, []);

  const reset = useCallback(() => {
    dispatch({ type: 'RESET' });
  }, []);

  return [state, setField, reset];
}

2.2 避免在 Hook 内部创建不必要的对象

每次渲染都创建新对象会导致依赖比较失效,引发无限循环。


// ❌ 错误:每次渲染创建新对象
function useData() {
  const [data, setData] = useState(null);
  useEffect(() => {
    fetchData().then(setData);
  }, []); // 依赖为空,但 fetchData 可能变化?
  return { data, setData };
}

// ✅ 正确:稳定引用
function useData() {
  const [data, setData] = useState(null);
  useEffect(() => {
    const fetchData = async () => {
      const result = await api.get('/data');
      setData(result);
    };
    fetchData();
  }, []); // 依赖稳定
  return { data, setData };
}

三、副作用处理:优雅地处理清理与竞态

3.1 清理函数防止内存泄漏

useEffect 中返回清理函数,取消订阅或异步操作。


function useWebSocket(url) {
  const [message, setMessage] = useState(null);

  useEffect(() => {
    const ws = new WebSocket(url);
    ws.onmessage = (event) => setMessage(event.data);

    return () => {
      ws.close(); // 组件卸载时关闭连接
    };
  }, [url]);

  return message;
}

3.2 使用 AbortController 取消请求

对于异步请求,使用 AbortController 避免竞态条件。


function useFetch(url) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    const abortController = new AbortController();
    const fetchData = async () => {
      try {
        const response = await fetch(url, { signal: abortController.signal });
        const result = await response.json();
        setData(result);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err);
        }
      }
    };
    fetchData();

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

  return { data, error };
}

四、参数设计:灵活且类型安全

4.1 使用选项对象模式

当参数较多时,使用对象参数提高可读性。


// ❌ 错误:参数过多,难以记忆
function useTimer(delay, interval, autoStart, onTick, onComplete) {}

// ✅ 正确:选项对象
function useTimer({ delay, interval, autoStart = false, onTick, onComplete }) {}

4.2 提供默认值与类型检查


function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(`Error reading localStorage key “${key}”:`, error);
      return initialValue;
    }
  });

  const setStoredValue = useCallback((newValue) => {
    setValue((prev) => {
      const valueToStore = newValue instanceof Function ? newValue(prev) : newValue;
      try {
        window.localStorage.setItem(key, JSON.stringify(valueToStore));
      } catch (error) {
        console.error(`Error setting localStorage key “${key}”:`, error);
      }
      return valueToStore;
    });
  }, [key]);

  return [value, setStoredValue];
}

五、测试策略:确保 Hook 的可靠性

5.1 使用 @testing-library/react-hooks


import { renderHook, act } from '@testing-library/react-hooks';
import useCounter from './useCounter';

test('should increment counter', () => {
  const { result } = renderHook(() => useCounter());

  act(() => {
    result.current.increment();
  });

  expect(result.current.count).toBe(1);
});

5.2 测试异步 Hook


import { renderHook } from '@testing-library/react-hooks';
import useFetch from './useFetch';

// 模拟 fetch
global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve({ data: 'test' }),
  })
);

test('should fetch data', async () => {
  const { result, waitForNextUpdate } = renderHook(() => useFetch('/api'));

  await waitForNextUpdate();

  expect(result.current.data).toEqual({ data: 'test' });
});

六、常见坑与踩坑经验

6.1 忘记在 useEffect 中指定依赖


function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []); // 正确:空依赖,因为 handleResize 不依赖任何值
  return width;
}

6.2 在渲染函数中直接调用 Hooks

Hooks 只能在函数组件或自定义 Hook 的顶层调用,不能在条件语句或循环中调用。


// ❌ 错误
if (condition) {
  useEffect(() => {});
}

// ✅ 正确
useEffect(() => {
  if (condition) {
    // 执行副作用
  }
}, [condition]);

6.3 过度抽象导致性能问题

不要为每个小功能都创建 Hook,避免不必要的性能开销。

七、总结

本文从设计原则、状态管理、副作用处理、参数设计、测试策略等方面,系统性地介绍了自定义 Hook 的最佳实践。关键要点:

  1. 单一职责:一个 Hook 只做一件事。
  2. 组合复用:利用多个基础 Hook 组合成复杂逻辑。
  3. 稳定引用:避免每次渲染创建新对象或函数。
  4. 清理副作用:及时取消订阅或请求。
  5. 灵活参数:使用选项对象并提供默认值。
  6. 充分测试:使用 @testing-library/react-hooks 确保可靠性。

延伸阅读

希望本文能帮助你写出更优雅、可维护的自定义 Hook。如果你有更好的实践,欢迎在评论区分享!