React Hooks 完全指南:从 useState 到自定义 Hook 的系统性掌握

系统性掌握 React Hooks:useState/useEffect/useContext/useRef/useMemo/useCallback/useReducer 深度用法、自定义 Hook 设计模式、性能陷阱与最佳实践、Hook 规则与 ESLint 配置。含 15+ 个实战代码示例。

Hooks(React 16.8 引入)是 React 现代开发的核心:它让函数组件拥有了状态管理、副作用处理、上下文消费、引用保存等全部能力,彻底终结了类组件的生命周期混乱。本文从每个内置 Hook 的深度用法,到自定义 Hook 设计模式,再到性能陷阱与最佳实践,帮你建立对 Hooks 的系统性理解。


一、Hooks 核心规则

Hooks 依赖调用顺序来维护内部状态,因此必须遵守两条铁律:

  1. 只在最顶层调用:不在 if/for/while 中调用
  2. 只在 React 函数中调用:函数组件或自定义 Hooks
function ValidComponent() {
  const [count, setCount] = useState(0);        // ✅
  const [name, setName] = useState('');         // ✅
  useEffect(() => {}, [count]);                 // ✅
  return <div>{count}</div>;
}

function InvalidComponent() {
  if (condition) {
    const [state] = useState(0);                // ❌ 在 if 中
  }
  return <div></div>;
}
# 安装 ESLint 插件自动检查
npm install -D eslint-plugin-react-hooks
// .eslintrc.js
module.exports = {
  rules: {
    'react-hooks/rules-of-hooks': 'error',
    'react-hooks/exhaustive-deps': 'warn',
  },
};

二、useState:状态管理

2.1 基础用法

const [count, setCount] = useState(0);

// 更新
setCount(count + 1);           // 替换为新值
setCount(c => c + 1);          // 函数式更新(基于前值,更安全)

2.2 对象状态更新

// ❌ 错误:直接修改对象
const [user, setUser] = useState({ name: 'Alice', age: 25 });
user.age = 26; // 未触发重新渲染!

// ✅ 正确:全新对象
setUser({ ...user, age: 26 });

// 或者用 immer
import { useImmer } from 'use-immer';
const [user, updateUser] = useImmer({ name: 'Alice', age: 25 });
updateUser(draft => { draft.age = 26; });

2.3 延迟初始化(避免重复计算)

// ❌ 每次渲染都调用 expensiveCalculation
const [state, setState] = useState(expensiveCalculation());

// ✅ 只在首次渲染调用
const [state, setState] = useState(() => expensiveCalculation());

2.4 状态同步问题:useState 是异步的

function BuggyComponent() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
    console.log(count); // ❌ 仍是旧值(0)
    // 修改在下次渲染时才生效
  }

  // ✅ 函数式更新解决
  function handleClickFixed() {
    setCount(c => {
      const next = c + 1;
      console.log(next); // 正确
      return next;
    });
  }
}

三、useEffect:副作用处理

3.1 三种执行时机

useEffect(() => {
  // 组件挂载时执行
  console.log('mounted');

  return () => {
    // 组件卸载时执行(清理)
    console.log('unmounted');
  };
}, []); // 空依赖数组 = 只在挂载/卸载执行

useEffect(() => {
  // 依赖变化时执行(包括首次挂载)
  console.log('count changed:', count);
}, [count]); // 依赖数组

useEffect(() => {
  // 每次渲染后都执行
  console.log('every render');
}); // 无依赖数组

3.2 常见副作用场景

// 数据获取
useEffect(() => {
  let cancelled = false;

  async function fetchData() {
    const res = await fetch(`/api/user/${userId}`);
    const data = await res.json();
    if (!cancelled) setUser(data);
  }

  fetchData();

  return () => { cancelled = true; };
}, [userId]);

// 订阅
useEffect(() => {
  const subscription = api.subscribe(handleUpdate);
  return () => subscription.unsubscribe();
}, []);

// DOM 操作
useEffect(() => {
  document.title = `Messages (${unreadCount})`;
}, [unreadCount]);

// 定时器
useEffect(() => {
  const id = setInterval(() => setTime(Date.now()), 1000);
  return () => clearInterval(id);
}, []);

3.3 避免无限循环

// ❌ 无限循环:setState → 重新渲染 → effect → setState
useEffect(() => {
  setCount(count + 1);
}, [count]);

// ✅ 条件更新
useEffect(() => {
  if (count < 10) setCount(count + 1);
}, [count]);

// ✅ 或用 useRef 标记
const didMount = useRef(false);
useEffect(() => {
  if (didMount.current) {
    setCount(prev => prev + 1);
  } else {
    didMount.current = true;
  }
}, [trigger]);

3.4 useEffect 的闭包陷阱

function StaleClosure() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setTimeout(() => {
      console.log(count); // ❌ 永远输出 0(挂载时的值)
    }, 3000);
    return () => clearTimeout(timer);
  }, []); // count 不在依赖数组中

  // ✅ 正确:把 count 加入依赖
  useEffect(() => {
    const timer = setTimeout(() => {
      console.log(count);
    }, 3000);
    return () => clearTimeout(timer);
  }, [count]);
}

规则:React Hooks ESLint 插件的 exhaustive-deps 规则会自动检测缺失的依赖,建议开启。


四、useContext:跨层级数据共享

// 创建上下文
const ThemeContext = createContext('light');

// Provider 包裹子树
function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

// 消费上下文
function Toolbar() {
  const theme = useContext(ThemeContext);
  return <div className={theme}>Toolbar</div>;
}

Context 性能陷阱

// ❌ 问题:value 每次渲染都是新对象,导致所有消费者重新渲染
function App() {
  const [user, setUser] = useState(null);
  return (
    <UserContext.Provider value={{ user, setUser }}> {/* 新对象 */}
      <DeepTree />
    </UserContext.Provider>
  );
}

// ✅ 方案 1:useMemo 缓存
const value = useMemo(() => ({ user, setUser }), [user]);
return <UserContext.Provider value={value}>...</UserContext.Provider>;

// ✅ 方案 2:拆分多个 Context
const UserStateContext = createContext(null);
const UserDispatchContext = createContext(null);
// 状态变化和 dispatch 分离,减少不必要的重渲染

五、useRef:DOM 引用与持久化值

5.1 DOM 引用

function InputFocus() {
  const inputRef = useRef(null);

  return (
    <>
      <input ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>Focus</button>
    </>
  );
}

5.2 保存上一次的值(不触发渲染)

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = useRef(count);

  useEffect(() => {
    prevCount.current = count; // 更新 ref,不触发渲染
  });

  return (
    <div>
      Now: {count}, Before: {prevCount.current}
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </div>
  );
}

5.3 保存定时器/订阅 ID

function Timer() {
  const timerId = useRef(null);

  const start = () => {
    timerId.current = setInterval(() => console.log('tick'), 1000);
  };

  const stop = () => {
    clearInterval(timerId.current);
  };

  return <><button onClick={start}>Start</button><button onClick={stop}>Stop</button></>;
}

六、useMemo & useCallback:性能优化

6.1 useMemo:缓存计算结果

// ❌ 每次渲染都排序(大数据量时卡顿)
const sorted = items.sort((a, b) => b.score - a.score);

// ✅ 只在 items 变化时排序
const sorted = useMemo(
  () => items.sort((a, b) => b.score - a.score),
  [items]
);

6.2 useCallback:缓存函数引用

// ❌ 每次渲染都是新函数,子组件 memo 失效
function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = () => console.log(count); // 新引用
  return <Child onClick={handleClick} />;
}

const Child = memo(function Child({ onClick }) {
  return <button onClick={onClick}>Click</button>;
});

// ✅ 缓存函数引用
function ParentFixed() {
  const [count, setCount] = useState(0);
  const handleClick = useCallback(() => console.log(count), [count]);
  return <Child onClick={handleClick} />;
}

6.3 什么时候不需要 useMemo/useCallback

// ❌ 过度使用:简单计算不需要缓存
const double = useMemo(() => count * 2, [count]); // 乘法成本极低

// ✅ 直接使用
const double = count * 2;

// ❌ 不要给所有函数加 useCallback
const handleClick = useCallback(() => {}, []); // 空函数的缓存没意义

缓存的适用场景

  • 计算成本高的派生数据(排序、筛选、图表数据)
  • 传递给 memo 子组件的函数和对象
  • 作为其他 Hooks 的依赖

七、useReducer:复杂状态逻辑

const initialState = { count: 0, step: 1 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + state.step };
    case 'decrement':
      return { ...state, count: state.count - state.step };
    case 'setStep':
      return { ...state, step: action.payload };
    default:
      throw new Error('Unknown action');
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      Step: <input
        value={state.step}
        onChange={e => dispatch({ type: 'setStep', payload: Number(e.target.value) })}
      />
    </>
  );
}

useReducer 适用场景:

  • 多个相关状态需要一起更新
  • 状态逻辑复杂(条件分支多)
  • 想复用状态逻辑到其他组件(抽为自定义 Hook)

八、自定义 Hooks:逻辑复用

8.1 设计原则

自定义 Hook 是命名以 use 开头的函数,可以调用其他 Hooks:

// useLocalStorage.ts
export function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = useCallback((value) => {
    setStoredValue(value);
    window.localStorage.setItem(key, JSON.stringify(value));
  }, [key]);

  return [storedValue, setValue];
}

// 使用
function Demo() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>{theme}</button>;
}

8.2 常用自定义 Hook 大全

// useDebounce — 防抖
function useDebounce(value, delay) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);
  return debounced;
}

// useFetch — 数据获取
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    fetch(url)
      .then(r => r.json())
      .then(data => { if (!cancelled) setData(data); })
      .catch(err => { if (!cancelled) setError(err); })
      .finally(() => { if (!cancelled) setLoading(false); });
    return () => { cancelled = true; };
  }, [url]);

  return { data, loading, error };
}

// useClickOutside — 点击外部关闭
defunction useClickOutside(ref, handler) {
  useEffect(() => {
    const listener = (e) => {
      if (!ref.current || ref.current.contains(e.target)) return;
      handler();
    };
    document.addEventListener('mousedown', listener);
    return () => document.removeEventListener('mousedown', listener);
  }, [ref, handler]);
}

// useMediaQuery — 响应式
function useMediaQuery(query) {
  const [matches, setMatches] = useState(false);
  useEffect(() => {
    const mql = window.matchMedia(query);
    setMatches(mql.matches);
    mql.addEventListener('change', (e) => setMatches(e.matches));
    return () => mql.removeEventListener('change', setMatches);
  }, [query]);
  return matches;
}

九、Hooks 性能陷阱与修复

陷阱 1:状态批量更新

// React 18 自动批处理
function BatchDemo() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    setCount(c => c + 1);
    setFlag(f => !f);
    // React 18 只重渲染一次
  }
}

陷阱 2:Context 引起的全局重渲染

修复:使用状态拆分 + use-context-selector

陷阱 3:useEffect 依赖数组遗漏

修复:开启 ESLint exhaustive-deps 规则,认真处理每个警告。


常见问题(FAQ)

自定义 Hook 和工具函数有什么区别?

自定义 Hook 内部可以调用 React Hooks(useState/useEffect 等),因此能参与 React 的渲染周期。普通工具函数不能调用 Hooks。

为什么 useEffect 中没有 async 函数?

useEffect 的回调不能直接是 async(因为它需要返回清理函数,Promise 会干扰)。但可以在内部定义 async 函数:

useEffect(() => {
  async function fetchData() { /* ... */ }
  fetchData();
}, []);

useMemo 和 useCallback 的区别?

  • useMemo:缓存
  • useCallback:缓存函数(本质是 useMemo(() => fn, deps)

useLayoutEffect 和 useEffect 区别?

  • useEffect:浏览器绘制后异步执行(不阻塞渲染)
  • useLayoutEffect:浏览器绘制前同步执行(用于测量 DOM 布局)

99% 场景用 useEffect,只有需要测量 DOM 尺寸并同步调整时才用 useLayoutEffect。

相关阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「frontend」更多文章