React Hooks 最佳实践与常见陷阱

4 阅读3分钟

引言

React Hooks 自 2019 年推出以来,彻底改变了我们编写 React 组件的方式。它们让函数组件拥有了状态管理和生命周期能力,代码更加简洁优雅。然而,Hooks 的使用并非没有陷阱——错误的依赖数组、滥用 useState、忽略清理函数等问题常常导致难以追踪的 bug。

本文将从实际项目经验出发,分享 React Hooks 的最佳实践,并剖析那些容易踩坑的地方,帮助你写出更健壮的代码。

一、useEffect 依赖数组的正確使用

常见错误

// ❌ 错误:遗漏依赖项
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, []); // userId 变化时不会重新请求
  
  return <div>{user?.name}</div>;
}

正确做法

// ✅ 正确:完整声明依赖
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    let cancelled = false;
    
    fetchUser(userId).then(data => {
      if (!cancelled) setUser(data);
    });
    
    return () => { cancelled = true; };
  }, [userId]);
  
  return <div>{user?.name}</div>;
}

要点:

  • 依赖数组必须包含所有在 effect 中使用的响应式值
  • 使用 ESLint 插件 eslint-plugin-react-hooks 自动检查
  • 对于异步操作,务必添加清理函数防止状态更新在组件卸载后执行

二、useState 的状态设计原则

避免派生状态

// ❌ 错误:存储可计算的值
function Cart({ items }) {
  const [total, setTotal] = useState(0);
  
  useEffect(() => {
    setTotal(items.reduce((sum, item) => sum + item.price, 0));
  }, [items]);
  
  return <div>总计:{total}</div>;
}

// ✅ 正确:直接计算
function Cart({ items }) {
  const total = items.reduce((sum, item) => sum + item.price, 0);
  return <div>总计:{total}</div>;
}

状态合并策略

// ❌ 多个相关状态
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState(null);

// ✅ 合并为单一状态对象
const [state, setState] = useState({
  loading: false,
  error: null,
  data: null
});

// 或使用 useReducer 处理复杂状态

三、自定义 Hooks 的复用艺术

提取通用逻辑

// useLocalStorage.js
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(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 ThemeToggle() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  
  return (
    <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
      当前主题:{theme}
    </button>
  );
}

命名规范

  • 自定义 Hooks 必须以 use 开头
  • 名称应清晰表达功能,如 useFetchuseFormuseDebounce

四、性能优化:useMemo 与 useCallback

何时使用

// ✅ 需要:计算密集型或引用稳定性
function ProductList({ products, filter }) {
  const filteredProducts = useMemo(() => {
    return products.filter(p => p.category === filter);
  }, [products, filter]);
  
  const handleSelect = useCallback((id) => {
    console.log('Selected:', id);
  }, []);
  
  return (
    <div>
      {filteredProducts.map(product => (
        <ProductItem key={product.id} product={product} onSelect={handleSelect} />
      ))}
    </div>
  );
}

避免过度优化

// ❌ 不必要:简单计算
const doubled = useMemo(() => count * 2, [count]); // 直接计算即可

// ❌ 不必要:内联函数无子组件依赖
<button onClick={() => handleClick()}>点击</button>

原则: 只有当计算开销大或需要稳定引用传递给子组件时,才使用记忆化 Hooks。

五、常见陷阱与解决方案

闭包陷阱

// ❌ 错误:捕获旧值
function Counter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const id = setInterval(() => {
      console.log(count); // 始终是 0
    }, 1000);
    return () => clearInterval(id);
  }, []);
  
  return <button onClick={() => setCount(c => c + 1)}>+1</button>;
}

// ✅ 正确:使用函数式更新或添加依赖
function Counter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1); // 使用函数式更新
    }, 1000);
    return () => clearInterval(id);
  }, []);
}

条件调用 Hooks

// ❌ 绝对禁止
if (condition) {
  const [value, setValue] = useState(0);
}

// ✅ 正确:始终在顶层调用
const [value, setValue] = useState(0);
if (condition) {
  // 条件逻辑放在这里
}

总结

React Hooks 是强大的工具,但需要正确使用才能发挥价值。记住以下核心原则:

  1. 依赖数组要完整——让 ESLint 帮你检查
  2. 避免派生状态——能计算就不要存储
  3. 提取自定义 Hooks——复用逻辑,保持组件简洁
  4. 谨慎优化——只在必要时使用 useMemo/useCallback
  5. 注意清理——异步操作和订阅必须清理
  6. 遵守规则——Hooks 只能在顶层调用

掌握这些实践,你的 React 代码将更加健壮、可维护。Hooks 不是银弹,但用对了,它们能让你的组件开发体验提升一个台阶。