返回首页

React Hooks 最佳实践

2026-06-05T00:00:00.000Z9 min read 分钟React, Hooks, 前端

深入理解 React Hooks,避免常见陷阱,写出更优雅的代码。

引言

React Hooks 自 2019 年发布以来,彻底改变了 React 组件的编写方式。函数组件配合 Hooks 已经成为 React 开发的主流范式。然而,Hooks 看似简洁的 API 背后,隐藏着不少容易踩坑的细节。本文将从实际项目经验出发,系统梳理 React Hooks 的最佳实践,帮助你写出更可靠、更优雅的代码。

useState vs useReducer:如何选择状态管理方案

useStateuseReducer 都用于管理组件状态,但它们适用于不同的场景。选择正确的工具可以大幅提升代码的可维护性。

useState 适用场景

当状态逻辑简单、状态之间相互独立时,useState 是最佳选择:

tsx
function SearchBar() {
  const [query, setQuery] = useState('');
  const [isFocused, setIsFocused] = useState(false);

  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      onFocus={() => setIsFocused(true)}
      onBlur={() => setIsFocused(false)}
      className={isFocused ? 'ring-2 ring-cyan-400' : ''}
    />
  );
}

useReducer 适用场景

当状态之间存在复杂关联、状态更新逻辑需要集中管理时,useReducer 更加合适。它通过将状态更新逻辑抽离到 reducer 函数中,使组件逻辑更加清晰:

tsx
interface FormState {
  values: Record<string, string>;
  errors: Record<string, string>;
  isSubmitting: boolean;
  isValid: boolean;
}

type FormAction =
  | { type: 'SET_FIELD'; field: string; value: string }
  | { type: 'SET_ERROR'; field: string; error: string }
  | { type: 'SUBMIT_START' }
  | { type: 'SUBMIT_SUCCESS' }
  | { type: 'SUBMIT_FAILURE'; errors: Record<string, string> }
  | { type: 'RESET' };

function formReducer(state: FormState, action: FormAction): FormState {
  switch (action.type) {
    case 'SET_FIELD':
      return {
        ...state,
        values: { ...state.values, [action.field]: action.value },
        errors: { ...state.errors, [action.field]: '' },
        isValid: true, // 清除错误后重新验证
      };
    case 'SUBMIT_START':
      return { ...state, isSubmitting: true };
    case 'SUBMIT_SUCCESS':
      return { ...state, isSubmitting: false, values: {}, errors: {} };
    case 'SUBMIT_FAILURE':
      return { ...state, isSubmitting: false, errors: action.errors };
    case 'RESET':
      return initialState;
    default:
      return state;
  }
}

const initialState: FormState = {
  values: {},
  errors: {},
  isSubmitting: false,
  isValid: true,
};

function ComplexForm() {
  const [state, dispatch] = useReducer(formReducer, initialState);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    dispatch({ type: 'SUBMIT_START' });
    try {
      await submitForm(state.values);
      dispatch({ type: 'SUBMIT_SUCCESS' });
    } catch (err) {
      dispatch({ type: 'SUBMIT_FAILURE', errors: (err as Error).message });
    }
  };

  // ...
}

一个简单的判断标准:如果你的组件中有 3 个以上相关的 useState,且它们的更新经常一起发生,那么考虑使用 useReducer 来替代。

useEffect 清理模式

useEffect 的清理函数是 Hooks 中最容易被忽视但极其重要的部分。不正确的清理会导致内存泄漏、竞态条件和状态更新已卸载组件等问题。

基本清理模式

每个 useEffect 都应该清理它创建的"副作用"。以下是几种常见的清理场景:

tsx
// 1. 清理定时器
useEffect(() => {
  const timer = setInterval(() => {
    fetchLatestData();
  }, 5000);

  return () => clearInterval(timer); // 组件卸载时清除定时器
}, []);

// 2. 清理事件监听器
useEffect(() => {
  const handleResize = () => setWidth(window.innerWidth);
  window.addEventListener('resize', handleResize);

  return () => window.removeEventListener('resize', handleResize);
}, []);

// 3. 清理 AbortController(防止竞态条件)
useEffect(() => {
  const controller = new AbortController();

  async function fetchData() {
    try {
      const res = await fetch('/api/data', {
        signal: controller.signal,
      });
      const data = await res.json();
      setData(data);
    } catch (err) {
      if ((err as Error).name !== 'AbortError') {
        setError(err as Error);
      }
    }
  }

  fetchData();

  return () => controller.abort(); // 依赖变化或卸载时取消请求
}, [endpoint]);

竞态条件处理

useEffect 的依赖发生变化时,旧的异步操作可能在新操作之后才返回结果。使用 AbortController 或标志变量来解决这个问题:

tsx
function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);

    fetchUser(userId).then((data) => {
      if (!cancelled) {
        setUser(data);
        setLoading(false);
      }
    });

    return () => {
      cancelled = true; // 防止过期数据覆盖新数据
    };
  }, [userId]);

  if (loading) return <UserSkeleton />;
  return <UserCard user={user!} />;
}

自定义 Hooks:复用逻辑的利器

自定义 Hooks 是 React 中复用逻辑的最佳方式。通过将通用的状态逻辑抽取为自定义 Hook,可以大幅减少代码重复。

useLocalStorage

一个实用的自定义 Hook,将 localStorage 读写封装为响应式状态:

tsx
function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    if (typeof window === 'undefined') return initialValue;
    try {
      const item = window.localStorage.getItem(key);
      return item ? (JSON.parse(item) as T) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = useCallback(
    (value: T | ((val: T) => T)) => {
      const valueToStore =
        value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    },
    [key, storedValue]
  );

  return [storedValue, setValue] as const;
}

// 使用示例
function ThemeSettings() {
  const [theme, setTheme] = useLocalStorage('theme', 'dark');
  return (
    <select value={theme} onChange={(e) => setTheme(e.target.value)}>
      <option value="dark">暗色模式</option>
      <option value="light">亮色模式</option>
    </select>
  );
}

useDebounce

另一个常用的自定义 Hook,用于防抖处理高频触发的值变化:

tsx
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// 使用示例:搜索输入防抖
function SearchComponent() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 300);

  useEffect(() => {
    if (debouncedQuery) {
      searchAPI(debouncedQuery).then(setResults);
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

性能优化 Hooks:useMemo 和 useCallback

useMemouseCallback 是 React 提供的两个性能优化 Hooks。但过度使用它们反而会导致性能下降(因为记忆化本身也有成本)。理解何时使用它们至关重要。

useMemo

useMemo 用于缓存计算结果,避免每次渲染都重新执行昂贵的计算:

tsx
function ProductList({ products, filter }: Props) {
  // 只在 products 或 filter 变化时重新计算
  const filteredProducts = useMemo(() => {
    return products
      .filter((p) => p.category === filter)
      .sort((a, b) => b.rating - a.rating);
  }, [products, filter]);

  return (
    <ul>
      {filteredProducts.map((product) => (
        <ProductItem key={product.id} product={product} />
      ))}
    </ul>
  );
}

useCallback

useCallback 用于缓存函数引用,常与 React.memo 配合使用,避免子组件不必要的重新渲染:

tsx
const TodoList = React.memo(function TodoList({
  todos,
  onToggle,
}: {
  todos: Todo[];
  onToggle: (id: string) => void;
}) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id} onClick={() => onToggle(todo.id)}>
          {todo.text}
        </li>
      ))}
    </ul>
  );
});

function App() {
  const [todos, setTodos] = useState<Todo[]>([]);

  // 使用 useCallback 保持函数引用稳定
  const handleToggle = useCallback((id: string) => {
    setTodos((prev) =>
      prev.map((todo) =>
        todo.id === id ? { ...todo, done: !todo.done } : todo
      )
    );
  }, []); // 空依赖数组,因为使用了函数式更新

  return <TodoList todos={todos} onToggle={handleToggle} />;
}

何时不需要优化

以下场景不需要使用 useMemouseCallback

  • 简单的计算(如字符串拼接、数字加减):记忆化的开销大于计算本身
  • 不会作为 props 传递给子组件的函数:没有子组件重新渲染的问题
  • 原始类型的值(字符串、数字、布尔值):React 已经对它们做了优化

总结

React Hooks 的最佳实践可以归纳为以下几点:

  1. 选择正确的状态管理工具:简单状态用 useState,复杂关联状态用 useReducer
  2. 始终正确处理 useEffect 清理:每个副作用都应该有对应的清理逻辑
  3. 善用自定义 Hooks:将可复用的状态逻辑抽取为自定义 Hook
  4. 合理使用性能优化 Hooks:先测量,再优化,不要过早使用 useMemouseCallback

掌握这些最佳实践,你将能够编写出更加健壮、可维护和高效的 React 代码。在后续的文章中,我将继续深入探讨 React 18 的并发特性以及 React Server Components 等前沿话题。