一、Hook 函数概述

1.1 Hook 定义

Hook 是 React 16.8 版本引入的新特性,本质是一套可以在函数组件中“钩入”React 内部状态和生命周期等特性的函数,无需编写 class 组件就能使用状态管理、副作用处理等核心能力,解决了 class 组件中逻辑复用复杂(如 HOC、Render Props 嵌套问题)、生命周期函数内逻辑混乱等痛点。

1.2 Hook 核心设计目标

  • 简化逻辑复用:替代高阶组件(HOC)和 Render Props,通过自定义 Hook 直接抽取和复用组件逻辑,避免组件嵌套层级过深。
  • 优化组件结构:将分散在不同生命周期的相关逻辑(如“数据请求-加载状态-错误处理”)聚合到同一 Hook 中,提升代码可读性。
  • 降低学习成本:无需理解 class 组件的 this 绑定、生命周期划分等复杂概念,用函数式思维开发 React 组件。

1.3 Hook 使用限制

Hook 需遵循严格规则,否则会破坏 React 对 Hook 调用顺序的追踪:

  • 只能在函数组件的顶层调用 Hook,不能在条件语句、循环、嵌套函数中调用(如 if (flag) { useState() } 是错误写法)。
  • 只能在React 函数组件自定义 Hook 中调用 Hook,不能在普通 JavaScript 函数中调用(如 function handleClick() { useEffect() } 是错误写法)。

二、常用内置 Hook 详解

React 提供了多个内置 Hook,覆盖状态管理、副作用处理、上下文访问等核心场景,以下是最常用的 6 个内置 Hook:

2.1 useState:状态管理 Hook

2.1.1 基本用法

useState 用于在函数组件中创建局部状态,返回一个“状态值 + 更新状态的函数”组成的数组,语法如下:

const [state, setState] = useState(initialState);
  • initialState:状态初始值,支持任意数据类型(数字、字符串、对象、数组等);若初始值计算昂贵(如复杂计算、数据转换),可传入函数(仅初始化时执行一次),例如 useState(() => computeExpensiveValue())
  • state:当前状态值,每次组件渲染时获取最新状态。
  • setState:状态更新函数,接收两种参数:
    • 直接传入新值(如 setState(10)):适用于状态与前值无关的场景。
    • 传入更新函数(如 setState(prevState => prevState + 1)):适用于状态更新依赖前值的场景(如计数器、累加逻辑),避免因 React 批量更新导致的状态覆盖问题。
2.1.2 注意事项
  • setState异步更新(在合成事件和生命周期中),若需在状态更新后获取最新值,需通过 useEffect 监听状态变化。
  • 状态更新是替换而非合并:若状态是对象(如 { name: 'A', age: 18 }),更新时需手动合并旧属性(如 setState(prev => ({ ...prev, age: 19 }))),否则会丢失未更新的属性(class 组件的 setState 会自动合并对象状态)。

2.2 useEffect:副作用处理 Hook

2.2.1 基本概念

“副作用”指组件渲染之外的操作,如数据请求、DOM 操作、事件监听、定时器等。useEffect 用于在函数组件中处理副作用,等效于 class 组件的 componentDidMount(挂载后)、componentDidUpdate(更新后)、componentWillUnmount(卸载前)三个生命周期的组合。

2.2.2 基本用法
useEffect(effectCallback, dependencies);
  • effectCallback:副作用执行函数,组件渲染后(首次渲染 + 更新渲染)会调用该函数;若函数返回一个清理函数,则在组件卸载前或下次 effectCallback 执行前调用,用于清除副作用(如取消定时器、移除事件监听)。
  • dependencies:依赖数组,控制 effectCallback 的执行时机:
    • 不传依赖数组:组件每次渲染(首次 + 所有更新)后都执行 effectCallback
    • 传空数组 []:仅在组件首次渲染(挂载)后执行 effectCallback,清理函数仅在组件卸载前执行(等效于 componentDidMount + componentWillUnmount)。
    • 传特定依赖(如 [count, user]):仅当依赖数组中的值发生变化时,才执行 effectCallback(等效于 componentDidMount + 监听依赖变化的 componentDidUpdate)。
2.2.3 常见场景示例
  1. 数据请求(挂载时请求一次,依赖变化时重新请求):
useEffect(() => {
  const fetchData = async () => {
    const res = await fetch(`/api/data?userId=${userId}`);
    const data = await res.json();
    setData(data);
  };
  fetchData();
}, [userId]); // 仅当 userId 变化时重新请求
  1. DOM 操作(监听窗口大小变化):
useEffect(() => {
  const handleResize = () => setWindowWidth(window.innerWidth);
  window.addEventListener('resize', handleResize);
  // 清理函数:卸载前移除事件监听
  return () => window.removeEventListener('resize', handleResize);
}, []); // 仅挂载时添加监听,卸载时移除

2.3 useContext:上下文访问 Hook

2.3.1 基本作用

useContext 用于在函数组件中直接访问 React Context 中的数据,无需通过 Context.Consumer 嵌套组件,简化上下文数据的传递和使用。

2.3.2 使用步骤
  1. 先通过 React.createContext 创建 Context 对象:
const ThemeContext = React.createContext('light'); // 'light' 是默认值(仅当无 Provider 时生效)
  1. 在组件树顶层用 ThemeContext.Provider 提供数据:
function App() {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Navbar /> {/* 子组件可访问 ThemeContext */}
    </ThemeContext.Provider>
  );
}
  1. 在子组件中用 useContext 访问数据:
function Navbar() {
  const { theme, setTheme } = useContext(ThemeContext);
  return (
    <div style={{ background: theme === 'light' ? 'white' : 'black' }}>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        切换主题
      </button>
    </div>
  );
}
2.3.3 注意事项
  • useContext 接收的是 Context 对象本身(如 ThemeContext),而非 ThemeContext.ProviderThemeContext.Consumer
  • Context.Providervalue 变化时,所有使用 useContext 访问该 Context 的组件都会重新渲染。

2.4 useReducer:复杂状态管理 Hook

2.4.1 基本作用

当组件状态逻辑复杂(如状态包含多个子值、状态更新依赖多个条件)时,useReduceruseState 更适合——它通过“ reducer 函数”统一管理状态更新逻辑,类似 Redux 的核心思想,让状态变化更可预测、更易调试。

2.4.2 基本用法
const [state, dispatch] = useReducer(reducer, initialState);
  • reducer:状态更新函数,接收两个参数 (state, action),返回新状态:
    • state:当前状态。
    • action:描述状态变化的“动作”,通常是 { type: '动作类型', payload: '附加数据' } 格式。
  • initialState:状态初始值。
  • state:当前状态值。
  • dispatch:触发状态更新的函数,通过传入 action 调用 reducer,例如 dispatch({ type: 'INCREMENT', payload: 1 })
2.4.3 示例:计数器(复杂状态场景)
// 1. 定义 reducer 函数
function countReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + action.payload };
    case 'DECREMENT':
      return { ...state, count: state.count - action.payload };
    case 'RESET':
      return { ...state, count: 0 };
    default:
      throw new Error('未知 action 类型');
  }
}

// 2. 在组件中使用 useReducer
function Counter() {
  const initialState = { count: 0, name: '计数器' };
  const [state, dispatch] = useReducer(countReducer, initialState);

  return (
    <div>
      <h2>{state.name}: {state.count}</h2>
      <button onClick={() => dispatch({ type: 'INCREMENT', payload: 1 })}>+1</button>
      <button onClick={() => dispatch({ type: 'DECREMENT', payload: 1 })}>-1</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>重置</button>
    </div>
  );
}

2.5 useRef:引用 Hook

2.5.1 基本作用

useRef 用于创建一个可变的 ref 对象,该对象的 current 属性可存储任意值(如 DOM 元素、定时器 ID、组件实例等),且 ref 对象的变化不会触发组件重新渲染,解决了“在函数组件中保存跨渲染周期的可变值”的需求。

2.5.2 两大核心场景
  1. 访问 DOM 元素(替代 class 组件的 this.refs):
function InputComponent() {
  const inputRef = useRef(null); // 初始值为 null

  const focusInput = () => {
    inputRef.current.focus(); // 通过 ref.current 访问 DOM 元素
  };

  return (
    <div>
      <input type="text" ref={inputRef} /> {/* 将 DOM 元素绑定到 ref */}
      <button onClick={focusInput}>聚焦输入框</button>
    </div>
  );
}
  1. 保存跨渲染的可变值(如定时器 ID、前一次状态):
function TimerComponent() {
  const [count, setCount] = useState(0);
  const timerRef = useRef(null); // 存储定时器 ID

  useEffect(() => {
    timerRef.current = setInterval(() => {
      setCount(prev => prev + 1);
    }, 1000);
    // 清理函数:清除定时器
    return () => clearInterval(timerRef.current);
  }, []);

  const stopTimer = () => {
    clearInterval(timerRef.current); // 访问跨渲染保存的定时器 ID
  };

  return (
    <div>
      <p>计时: {count}s</p>
      <button onClick={stopTimer}>停止计时</button>
    </div>
  );
}
2.5.3 注意事项
  • ref.current 的变化不会触发组件重新渲染,若需在 ref.current 变化时执行逻辑,需配合 useEffect 监听。

2.6 useMemo & useCallback:性能优化 Hook

两者均用于避免组件不必要的重新计算或重新渲染,核心是“缓存计算结果或函数引用”,仅在依赖变化时更新。

2.6.1 useMemo:缓存计算结果
  • 作用:当组件中有昂贵的计算逻辑(如复杂数组排序、数据转换)时,useMemo 可缓存计算结果,避免每次渲染都重复执行计算。
  • 语法:const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • 逻辑:仅当依赖数组 [a, b] 中的值变化时,才重新执行 computeExpensiveValue 并更新缓存;否则直接返回缓存的结果。
2.6.2 useCallback:缓存函数引用
  • 作用:当组件将函数作为 props 传递给子组件时,useCallback 可缓存函数引用,避免每次父组件渲染都创建新函数,导致子组件(尤其是使用 React.memo 优化的子组件)不必要的重新渲染。
  • 语法:const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]);
  • 逻辑:仅当依赖数组 [a, b] 中的值变化时,才创建新函数并更新缓存;否则直接返回缓存的函数引用。
2.6.3 示例:父子组件性能优化
// 子组件:用 React.memo 包裹,仅当 props 变化时重新渲染
const Child = React.memo(({ onClick, data }) => {
  console.log('Child 重新渲染');
  return <button onClick={onClick}>{data}</button>;
});

// 父组件:用 useCallback 缓存函数,用 useMemo 缓存计算结果
function Parent() {
  const [count, setCount] = useState(0);

  // 缓存函数:仅当 count 变化时,函数引用才更新
  const handleClick = useCallback(() => {
    console.log('点击事件,count:', count);
  }, [count]);

  // 缓存计算结果:仅当 count 变化时,才重新计算 sortedData
  const sortedData = useMemo(() => {
    return [1, 3, 2].sort((a, b) => a - b + count); // 模拟昂贵计算
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(prev => prev + 1)}>增加 count</button>
      <Child onClick={handleClick} data={sortedData} />
    </div>
  );
}

三、自定义 Hook 开发

3.1 自定义 Hook 定义与规则

自定义 Hook 是遵循 Hook 规则的普通 JavaScript 函数,名称必须以 use 开头(如 useFetchuseTimer),内部可调用其他内置 Hook 或自定义 Hook,目的是抽取和复用组件间的通用逻辑

3.1.1 核心规则
  • 名称必须以 use 开头:React 依赖该命名规则识别 Hook,确保 ESLint 插件(如 eslint-plugin-react-hooks)能检查 Hook 使用是否合规。
  • 内部可调用其他 Hook:如在自定义 Hook 中使用 useStateuseEffect 等,但需遵循 Hook 的使用限制(不能在条件、循环中调用)。

3.2 自定义 Hook 示例

示例 1:useFetch(数据请求 Hook)

抽取“数据请求-加载状态-错误处理”的通用逻辑:

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

  useEffect(() => {
    // 重置状态
    setLoading(true);
    setError(null);

    const fetchData = async () => {
      try {
        const res = await fetch(url);
        if (!res.ok) throw new Error('请求失败');
        const result = await res.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url, ...dependencies]); // 依赖 url 和外部传入的 dependencies

  // 返回请求相关状态和数据
  return { data, loading, error };
}

// 使用自定义 Hook
function UserList() {
  const { data: users, loading, error } = useFetch('/api/users');

  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
示例 2:useLocalStorage(本地存储 Hook)

抽取“读取/写入 localStorage”的逻辑,实现状态与本地存储同步:

function useLocalStorage(key, initialValue) {
  // 从 localStorage 读取初始值(优先级高于 initialValue)
  const [value, setValue] = useState(() => {
    const storedValue = localStorage.getItem(key);
    return storedValue ? JSON.parse(storedValue) : initialValue;
  });

  // 当 value 变化时,同步写入 localStorage
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  // 返回状态和更新函数
  return [value, setValue];
}

// 使用自定义 Hook
function ThemeSetting() {
  // 状态与 localStorage 的 'theme' 键同步
  const [theme, setTheme] = useLocalStorage('theme', 'light');

  return (
    <div>
      <p>当前主题: {theme}</p>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        切换主题
      </button>
    </div>
  );
}

四、Hook 高级特性与最佳实践

4.1 Hook 调用顺序的重要性

React 依赖每次组件渲染时 Hook 调用顺序的一致性来追踪状态(如 useState 的第 1 次调用对应第 1 个状态,第 2 次调用对应第 2 个状态)。若在条件语句中调用 Hook,会导致每次渲染时 Hook 调用顺序变化,进而引发状态错乱或报错。

错误示例(条件中调用 Hook):

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

  if (count > 5) {
    // 错误:Hook 不能在条件中调用,当 count > 5 时会多调用一次 useEffect
    useEffect(() => {
      console.log('Count 超过 5');
    }, []);
  }

  return <button onClick={() => setCount(prev => prev + 1)}>{count}</button>;
}

正确示例(在 Hook 内部处理条件):

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

  useEffect(() => {
    // 正确:在 Hook 内部判断条件,保证 Hook 调用顺序一致
    if (count > 5) {
      console.log('Count 超过 5');
    }
  }, [count]);

  return <button onClick={() => setCount(prev => prev + 1)}>{count}</button>;
}

4.2 避免过度使用 useMemo/useCallback

  • useMemouseCallback 本身有性能开销(需存储缓存、对比依赖),若计算逻辑简单(如 a + b)或子组件无需优化(未用 React.memo),过度使用会适得其反。
  • 建议仅在以下场景使用:
    • useMemo:处理昂贵的计算逻辑(如耗时超过 1ms 的操作)。
    • useCallback:函数作为 props 传递给用 React.memo 优化的子组件,或函数作为依赖传入其他 Hook(如 useEffect)。

4.3 拆分复杂 useEffect

若一个 useEffect 中包含多个不相关的副作用(如“数据请求”和“DOM 监听”),会导致逻辑混乱、依赖数组臃肿。建议按“功能相关性”拆分多个 useEffect,每个 useEffect 只处理一类副作用。

错误示例(一个 useEffect 处理多个副作用):

useEffect(() => {
  // 副作用 1:数据请求
  const fetchData = async () => {
    const res = await fetch('/api/data');
    const data = await res.json();
    setData(data);
  };
  fetchData();

  // 副作用 2:DOM 监听
  const handleResize = () => setWindowWidth(window.innerWidth);
  window.addEventListener('resize', handleResize);

  // 清理函数:混合两种副作用的清理逻辑
  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, [userId]); // 依赖仅与数据请求相关,与 DOM 监听无关

正确示例(拆分多个 useEffect):

// 副作用 1:数据请求(依赖 userId)
useEffect(() => {
  const fetchData = async () => {
    const res = await fetch(`/api/data?userId=${userId}`);
    const data = await res.json();
    setData(data);
  };
  fetchData();
}, [userId]);

// 副作用 2:DOM 监听(无依赖)
useEffect(() => {
  const handleResize = () => setWindowWidth(window.innerWidth);
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);

4.4 自定义 Hook 的复用边界

自定义 Hook 应聚焦于“通用逻辑”,避免包含与特定业务强耦合的代码(如特定 API 地址、业务常量)。可通过参数将业务相关配置(如 urlheaders)传入,提升 Hook 的复用性。

五、Hook 与 Class 组件的对比

特性Hook(函数组件)Class 组件
状态管理useState 创建局部状态,支持多个独立状态this.state 管理状态,需手动绑定 this
副作用处理useEffect 聚合相关逻辑分散在 componentDidMount/Update/WillUnmount
逻辑复用自定义 Hook 直接复用,无嵌套依赖 HOC/Render Props,易产生“嵌套地狱”
代码体积代码简洁,无 class 模板代码需编写 constructorrender 等模板代码
学习成本只需理解函数和 Hook 规则需理解 this 绑定、生命周期、继承等
调试体验状态与函数作用域绑定,调试直观需追踪 this.state 变化,调试较复杂

六、总结

React Hook 是函数组件的核心增强特性,通过 useStateuseEffect 等内置 Hook 解决了 class 组件的痛点,同时支持自定义 Hook 实现逻辑复用。掌握 Hook 的关键在于:

  1. 严格遵循 Hook 使用规则(顶层调用、仅在函数组件/自定义 Hook 中调用)。
  2. 理解各内置 Hook 的适用场景(如 useState 管状态、useEffect 管副作用)。
  3. 合理设计自定义 Hook,抽取通用逻辑,提升代码复用性。
  4. 结合性能优化 Hook(useMemo/useCallback)和最佳实践,写出高效、可维护的 React 代码。

目前,Hook 已成为 React 开发的主流方案,React 官方也推荐优先使用 Hook 开发新组件。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐