【React】一篇文章带你了解常见的Hooks函数

682 阅读7分钟

React Hooks 详解:从基础到进阶 🚀

React Hooks 是 React 16.8 引入的重要特性,它允许开发者在函数组件中使用状态(State)和生命周期逻辑,而无需编写类组件。

Hooks 的核心思想

  • 把复杂的逻辑“封装”成可复用的函数。
  • 让函数组件像类组件一样强大,但代码更简洁!

1. useState:管理组件状态 💡

用途

useState 是最基础的 Hook,用于在函数组件中声明和更新状态变量。它返回一个包含当前状态值和更新函数的数组。

代码示例

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0); // 声明状态变量 count,初始值为 0

  return (
    <div>
      <p>当前计数:{count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
      <button onClick={() => setCount(count - 1)}>减少</button>
    </div>
  );
}

深入解析

  1. 状态更新是异步的
    React 可能会将多个 setCount 调用合并以提高性能。例如,连续调用 setCount(count + 1) 两次,最终只更新一次状态。

  2. 函数式更新
    如果新状态依赖于旧状态,使用函数式更新可以确保获取最新的值。例如:

    setCount(prevCount => prevCount + 1);
    
  3. 多个状态变量
    可以在同一个组件中声明多个状态变量:

    const [name, setName] = useState('John');
    const [age, setAge] = useState(25);
    
  4. 不可变更新原则
    状态更新必须通过 setState 函数,不能直接赋值(如 count = 1)。

注意事项

  • 性能优化:如果状态更新频繁且耗时,考虑使用 useReducer 替代 useState

2. useEffect:处理副作用 ⚙️

用途

useEffect 用于在组件渲染后执行副作用操作,例如数据请求、订阅事件、DOM 操作等。它结合了类组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 的功能。

代码示例

import React, { useState, useEffect } from 'react';

function DataFetcher() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/todos/1')
      .then(res => res.json())
      .then(json => setData(json.title));
  }, []); // 空数组表示只在组件挂载时执行一次

  return (
    <div>
      {data ? <p>获取到的数据:{data}</p> : <p>加载中...</p>}
    </div>
  );
}

深入解析

  1. 依赖数组的作用

    • 空数组 []:副作用仅在组件挂载时执行一次。
    • 包含变量的数组 [count]:只有当 count 变化时,副作用才会重新执行。
    • 省略数组:副作用会在每次渲染后执行。
  2. 清理副作用
    如果副作用需要清理(如取消订阅、移除事件监听器),返回一个清理函数:

    useEffect(() => {
      const timer = setInterval(() => {
        console.log('定时器触发');
      }, 1000);
      return () => clearInterval(timer); // 组件卸载时清除定时器
    }, []);
    
  3. 同步 vs 异步
    useEffect 中的副作用是异步执行的,不会阻塞 DOM 更新。如果需要立即操作 DOM,使用 useLayoutEffect

注意事项

  • 避免过度使用:每个 useEffect 都会增加组件的复杂性,尽量合并相关逻辑。
  • 依赖项缺失:如果依赖数组未包含所有相关变量,可能导致副作用无法正确执行或内存泄漏。

3. useReducer:管理复杂状态逻辑 🧠

用途

useReducer 适用于复杂的状态逻辑(如表单验证、购物车计算)。它通过 reducer 函数统一管理状态更新,类似于 Redux 的模式。

代码示例

import React, { useReducer } from 'react';

// 定义 reducer 函数
function counterReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <p>当前计数:{state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>增加</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>减少</button>
    </div>
  );
}

深入解析

  1. 纯函数原则
    reducer 必须是一个纯函数,不能直接修改 state,而是返回新对象。

  2. 嵌套状态更新
    对于嵌套对象的状态更新,推荐使用 immer 库简化代码。例如:

    import produce from 'immer';
    
    function reducer(state, action) {
      return produce(state, draft => {
        draft.nested.value = action.value;
      });
    }
    
  3. 初始化状态
    可以通过第三个参数 init 提供初始化函数:

    const [state, dispatch] = useReducer(reducer, initialState, init);
    

注意事项

  • 避免过度设计:对于简单的状态逻辑(如计数器),useState 更直观。
  • 调试困难:复杂的 reducer 可能导致调试困难,建议拆分为多个小函数。

4. useRef:访问 DOM 或存储可变值 🎯

用途

useRef 返回一个可变对象(.current 属性),常用于:

  1. 访问 DOM 元素
  2. 存储不随渲染变化的值(如定时器 ID、表单输入值)

代码示例

import React, { useRef } from 'react';

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

  const focusInput = () => {
    inputRef.current.focus(); // 直接操作 DOM
  };

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="点击按钮聚焦" />
      <button onClick={focusInput}>聚焦输入框</button>
    </div>
  );
}

深入解析

  1. 不会触发重新渲染
    修改 .current 的值不会导致组件重新渲染。

  2. 跨渲染保持值
    useRef 可以存储任何可变值(如 counttimeoutId),适合需要跨渲染保持的场景。

  3. useState 的对比

    • useRef 适合存储不需要触发渲染的值。
    • useState 适合存储需要触发渲染的状态。

注意事项

  • 避免滥用:如果值需要触发渲染,优先使用 useState
  • 内存泄漏风险:存储的 DOM 元素或事件监听器需在组件卸载时手动清理。

5. useContext:跨组件共享状态 🌐

用途

useContext 用于在组件树中共享状态,避免通过 props 一层层传递(即“props drilling”问题)。

代码示例

import React, { createContext, useContext } from 'react';

// 创建 Context
const ThemeContext = createContext('light');

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Page />
    </ThemeContext.Provider>
  );
}

function Page() {
  const theme = useContext(ThemeContext);
  return <p>当前主题:{theme}</p>;
}

深入解析

  1. 创建 Context
    使用 createContext 创建一个 Context 对象,提供默认值。

  2. 提供 Context 值
    使用 Provider 组件包裹子组件树,并通过 value 属性传递共享的状态。

  3. 消费 Context 值
    使用 useContext 钩子获取 Context 的值,无需逐层传递 props

注意事项

  • 性能影响:如果 Context 的值频繁变化,所有消费组件都会重新渲染。可以通过 useMemouseCallback 优化。
  • 避免过度使用:对于局部状态(如组件内部状态),优先使用 useState

6. useLayoutEffect:同步 DOM 更新 ⚡️

用途

useLayoutEffectuseEffect 类似,但会在 DOM 更新之前同步执行,适合需要立即操作 DOM 的场景(如测量 DOM 宽度/高度)。

代码示例

import React, { useLayoutEffect, useRef } from 'react';

function MeasureBox() {
  const boxRef = useRef(null);

  useLayoutEffect(() => {
    const width = boxRef.current.offsetWidth;
    console.log('盒子宽度:', width);
  }, []);

  return <div ref={boxRef} style={{ width: '200px', height: '100px' }} />;
}

深入解析

  1. 同步执行
    useLayoutEffect 是同步的,会阻塞浏览器的绘制,直到所有 DOM 更新完成。

  2. 适用场景

    • 需要测量 DOM 尺寸(如弹窗定位)。
    • 需要同步更新样式(避免页面闪烁)。
  3. 清理副作用
    同样支持返回清理函数,用于卸载时的资源释放。

注意事项

  • 性能开销useLayoutEffect 会阻塞渲染,谨慎使用以避免性能问题。
  • useEffect 的选择:优先使用 useEffect,仅在需要同步操作 DOM 时使用 useLayoutEffect

7. useMemo:缓存计算结果 🧮

用途

useMemo 用于缓存计算结果,避免在每次组件渲染时重复执行昂贵的计算操作。它类似于类组件中的 shouldComponentUpdate,但更灵活。

代码示例

import React, { useState, useMemo } from 'react';

function ExpensiveComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // 使用 useMemo 缓存计算结果
  const expensiveValue = useMemo(() => {
    console.log('计算中...');
    let result = 0;
    for (let i = 0; i < 100000000; i++) {
      result += i;
    }
    return result;
  }, [count]); // 仅在 count 变化时重新计算

  return (
    <div>
      <p>计数:{count}</p>
      <button onClick={() => setCount(count + 1)}>增加计数</button>
      <p>文本:{text}</p>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <p>计算结果:{expensiveValue}</p>
    </div>
  );
}

深入解析

  1. 缓存机制
    useMemo 会在依赖项([count])发生变化时重新执行计算,并缓存结果。如果依赖项未变化,则直接返回缓存值。

  2. 适用场景

    • 昂贵的计算:如大数据排序、复杂算法、频繁的字符串拼接等。
    • 避免重复渲染:当某个值仅在特定条件下需要更新时。
  3. 注意事项

    • 避免滥用:如果计算逻辑简单,直接计算比使用 useMemo 更高效。
    • 依赖项管理:确保依赖数组正确,否则可能导致缓存失效或内存泄漏。

总结

Hook用途适合场景
useState管理简单状态计数器、表单输入
useEffect处理副作用数据请求、事件监听
useReducer管理复杂状态逻辑表单验证、购物车
useRef访问 DOM 或存储可变值输入框聚焦、定时器 ID
useContext跨组件共享状态主题切换、用户登录状态
useLayoutEffect同步 DOM 更新测量 DOM 尺寸、同步样式更新
useMemo缓存计算结果昂贵的计算、避免重复渲染

🚀 最后的小贴士

  • 多写代码:动手实践是掌握 Hooks 的唯一途径。
  • 查文档:遇到问题时,先看官方文档,再翻社区文章。
  • 看案例:通过真实项目理解技术如何落地。

🎉 学习就像练武功,坚持就是胜利!