React 架构进阶:自定义 Hooks 的高级设计模式与最佳实践

28 阅读5分钟

在 React 16.8 引入 Hooks 之后,我们告别了 Class 组件中复杂的生命周期和高阶组件(HOC)的嵌套地狱。然而,随着业务复杂度的提升,简单的 useState 和 useEffect 组合往往导致组件内部逻辑臃肿,难以维护。

很多开发者停留在“把逻辑抽离成函数”的初级阶段,却忽略了自定义 Hooks(Custom Hooks)本质上是逻辑复用的设计模式。本文将深入探讨自定义 Hooks 的高级设计模式,如何通过合理的抽象提升代码的可读性、可测试性和复用性。

一、为什么我们需要高级设计模式?

在初级实践中,我们常看到这样的代码:

// ❌ 反模式:逻辑泄露与耦合
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    fetch(`/api/user/${userId}`).then(setUser).finally(() => setLoading(false));
  }, [userId]);

  // ... 还有获取用户帖子、获取用户关注列表的逻辑混在一起
  return loading ? <Spinner /> : <div>{user.name}</div>;
}

这种写法的问题在于:

  1. UI 与逻辑耦合:组件既负责渲染,又负责数据获取。
  2. 难以测试:很难在不渲染 UI 的情况下测试数据获取逻辑。
  3. 无法复用:如果在另一个页面也需要获取用户信息,代码只能复制粘贴。

通过自定义 Hooks,我们可以将“关注点分离(Separation of Concerns)”。

二、核心设计模式详解

2.1 容器模式(Container Pattern)的 Hooks 化

这是最经典的模式,将数据获取和状态管理逻辑剥离,组件只负责展示。

// ✅ useUser.ts - 专注数据逻辑
export function useUser(userId) {
  const [state, setState] = useState({ data: null, loading: true, error: null });

  useEffect(() => {
    let cancelled = false;
    
    async function fetchUser() {
      try {
        const response = await fetch(`/api/user/${userId}`);
        if (!cancelled) {
          setState({ data: await response.json(), loading: false, error: null });
        }
      } catch (err) {
        if (!cancelled) setState({ data: null, loading: false, error: err });
      }
    }

    fetchUser();
    return () => { cancelled = true; }; // 清理副作用
  }, [userId]);

  return state;
}

// ✅ UserProfile.tsx - 专注 UI 展示
function UserProfile({ userId }) {
  const { data: user, loading, error } = useUser(userId);

  if (loading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  return <div>{user.name}</div>;
}

优势:UI 组件变得极其纯净,逻辑 Hook 可以独立进行单元测试。

2.2 状态机模式(State Machine Pattern)

对于复杂的交互流程(如表单提交、多步骤向导、播放器控制),简单的布尔值状态(isLoadingisSuccessisError)容易导致状态冲突。此时应引入有限状态机思想。

// ✅ useAsyncAction.ts - 管理复杂状态流转
function useAsyncAction(asyncFunction) {
  const [state, dispatch] = useReducer((state, action) => {
    switch (action.type) {
      case 'START': return { status: 'loading', data: null, error: null };
      case 'SUCCESS': return { status: 'success', data: action.payload, error: null };
      case 'FAILURE': return { status: 'failure', data: null, error: action.payload };
      case 'RESET': return { status: 'idle', data: null, error: null };
      default: return state;
    }
  }, { status: 'idle', data: null, error: null });

  const execute = useCallback(async (...args) => {
    dispatch({ type: 'START' });
    try {
      const result = await asyncFunction(...args);
      dispatch({ type: 'SUCCESS', payload: result });
    } catch (err) {
      dispatch({ type: 'FAILURE', payload: err });
    }
  }, [asyncFunction]);

  return { ...state, execute };
}

应用场景:登录注册流程、文件上传、复杂的表单验证。它保证了状态流转的确定性,避免了“既 loading 又 error”的非法状态。

2.3 组合模式(Composition Pattern)

Hooks 最大的威力在于组合。我们可以像搭积木一样,将多个小 Hooks 组合成一个功能强大的大 Hook。

// 基础 Hook:处理本地存储
function useLocalStorage(key, initialValue) {
  // ... 实现略
  return [value, setValue];
}

// 基础 Hook:处理窗口大小
function useWindowSize() {
  // ... 实现略
  return { width, height };
}

// ✅ 组合 Hook:响应式主题管理器
function useResponsiveTheme() {
  const [theme, setTheme] = useLocalStorage('app-theme', 'light');
  const { width } = useWindowSize();

  // 自动逻辑:屏幕小于 768px 强制使用移动端样式,但保留用户主题偏好
  const isMobile = width < 768;
  const effectiveTheme = isMobile ? 'mobile-optimized' : theme;

  useEffect(() => {
    document.body.className = effectiveTheme;
  }, [effectiveTheme]);

  return { theme, setTheme, isMobile };
}

核心价值:降低了单个 Hook 的认知负荷,每个 Hook 只做一件事,并通过组合产生新的行为。

2.4 观察者模式与订阅机制

在处理全局事件或非 React 源的数据(如 WebSocket、第三方 SDK)时,可以使用观察者模式。

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

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

    ws.onerror = (error) => {
      console.error('WS Error', error);
    };

    // 清理连接
    return () => {
      ws.close();
    };
  }, [url]);

  const sendMessage = useCallback((data) => {
    // 发送逻辑
  }, []);

  return { message, sendMessage };
}

三、避坑指南:自定义 Hooks 的常见陷阱

3.1 条件调用 Hooks

错误示范

function useConditionalHook(condition) {
  if (condition) {
    useEffect(() => { ... }); // ❌ 违反 Rules of Hooks
  }
}

修正:Hooks 必须在顶层调用。如果需要根据条件执行逻辑,请将条件判断写在 Hook 内部,而不是包裹 Hook 本身。

3.2 过度抽象

不要为了复用而复用。如果一个逻辑只在当前组件使用,或者不同组件的使用差异极大,强行提取 Hook 反而会增加认知负担。 “三次法则” 是一个不错的经验:当同一段逻辑出现第三次时,再考虑提取。

3.3 依赖项数组的陷阱

在自定义 Hook 中返回回调函数时,务必注意闭包陷阱。

// ❌ 容易捕获旧状态的回调
function useCounter() {
  const [count, setCount] = useState(0);
  const logCount = () => {
    console.log(count); // 可能永远是初始值或旧值
  };
  return { count, logCount };
}

// ✅ 使用 ref 或将其放入 useEffect/useCallback 依赖中
function useCounter() {
  const [count, setCount] = useState(0);
  
  const logCount = useCallback(() => {
    console.log(count); 
  }, [count]); // 确保依赖最新 count
  
  return { count, logCount };
}

四、实战案例:构建一个通用的 useFetch

结合上述模式,我们来构建一个生产级别的 useFetch

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

// 定义状态类型
const initialState = {
  data: null,
  loading: false,
  error: null,
};

function reducer(state, action) {
  switch (action.type) {
    case 'REQUEST': return { ...state, loading: true, error: null };
    case 'SUCCESS': return { loading: false, data: action.payload, error: null };
    case 'FAILURE': return { loading: false, data: null, error: action.payload };
    case 'RESET': return initialState;
    default: return state;
  }
}

export function useFetch(url, options = {}) {
  const [state, dispatch] = useReducer(reducer, initialState);
  const { manual = false } = options; // 是否手动触发

  const execute = useCallback(async (overrideUrl) => {
    const targetUrl = overrideUrl || url;
    if (!targetUrl) return;

    dispatch({ type: 'REQUEST' });
    try {
      const response = await fetch(targetUrl);
      if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
      const data = await response.json();
      dispatch({ type: 'SUCCESS', payload: data });
    } catch (err) {
      dispatch({ type: 'FAILURE', payload: err.message });
    }
  }, [url]);

  useEffect(() => {
    if (!manual) {
      execute();
    }
  }, [execute, manual]);

  return { ...state, refetch: execute, reset: () => dispatch({ type: 'RESET' }) };
}

使用示例

function UserList() {
  const { data, loading, error, refetch } = useFetch('/api/users');

  if (loading) return <div>加载中...</div>;
  if (error) return <div>出错了: {error} <button onClick={refetch}>重试</button></div>;

  return (
    <ul>
      {data.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

五、总结

自定义 Hooks 不仅仅是代码复用的工具,更是 React 组件架构的核心支柱。

  1. 逻辑解耦:让 UI 组件回归纯粹的表现层。
  2. 状态治理:利用 Reducer 和状态机管理复杂交互。
  3. 能力组合:通过小 Hook 的堆叠构建复杂功能。
  4. 测试友好:逻辑与视图分离使得单元测试变得简单高效。

掌握这些高级模式,你将能够编写出更健壮、更易维护的 React 应用,真正发挥 Hooks 体系的威力。


后续思考题:

  • 如何在自定义 Hook 中处理服务端渲染(SSR)时的 Hydration 问题?
  • 自定义 Hooks 能否完全替代 Redux/MobX 等全局状态管理库?边界在哪里?

欢迎在评论区分享你在项目中封装过的最得意的自定义 Hook!