一、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 垂直技术社区,欢迎活跃、内容共建。

更多推荐