React常用Hooks函数(1):useState、useEffect、useLayoutEffect……

108 阅读7分钟

什么是 Hooks?

简单来说,Hooks 是让函数组件拥有类组件功能的特殊函数。在 Hooks 出现之前,函数组件只能是无状态的,想要使用状态管理、生命周期等功能,就必须使用类组件。而有了 Hooks,我们可以在函数组件中轻松实现这些功能,让代码更加简洁、易懂。

想象一下,组件的生命周期就像一条线,从挂载、更新到卸载,而 Hooks 就像一个个钩子,能在这条线上的特定位置挂接我们需要执行的代码,这也是 “钩子” 这个名字的由来。

常用 Hooks 详解

1. useState:组件的状态管家

useState是最基础也最常用的 Hooks 之一,它的作用是为函数组件添加状态管理功能。

基本用法

const [state, setState] = useState(initialValue);

这里的state是我们定义的状态变量,setState是用于更新状态的函数,initialValue是状态的初始值,决定state的类型。

特点

  • 不支持异步更新后的立即获取新值。如果需要基于前一个状态计算新状态,应该使用函数形式:
setState(prevState => prevState + 1);
  • 每次调用setState都会触发组件重新渲染。

  • 可以在一个组件中多次使用useState,管理不同的状态:

const [name, setName] = useState('');


const [age, setAge] = useState(18);

使用场景:管理组件中的各种状态,如表单输入值、开关状态、计数器等简单状态。

2. useEffect:处理副作用的多面手

useEffect被称为 “副作用函数”,能在组件的不同生命周期阶段执行我们需要的操作。

基本用法

useEffect(() => {
 // 函数体:要执行的操作
 return () => {
  // 清理函数:组件卸载时执行的操作
 };
}, [依赖项]);

作用:在函数组件中模拟类组件的生命周期函数,让我们可以在组件的不同阶段执行代码。

执行时机

  1. 组件每次加载(挂载)时会触发。

  2. useEffect的第二个参数(依赖项数组)中的值发生变化时,会重新执行。

  3. 当依赖项为空数组[]时,只会在组件初次渲染时执行一次。

  4. 第一个参数函数内部返回的新函数,会在组件被卸载时执行,用于清理资源。

常见用途

  • 数据获取:在组件挂载后从服务器获取数据

  • 订阅事件:如监听窗口大小变化、滚动事件等

  • DOM 操作:对 DOM 元素进行修改

  • 清理操作:如清除定时器、取消订阅等

示例


useEffect(() => {
// 组件挂载时获取数据
    fetchData();
// 组件卸载时取消订阅
    return () => {
        unsubscribe();
    };

}, []); // 空数组表示只在初次渲染时执行

3. useLayoutEffect:同步执行的 DOM 操作专家

useLayoutEffect的用法和useEffect非常相似,但它们的执行时机有所不同。

特点

  • useLayoutEffect中的函数会在所有 DOM 变更后同步执行,而且是在浏览器绘制之前。

  • useEffect中的函数是在浏览器绘制之后异步执行的。

使用场景


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

function LayoutEffectDemo() {
  const [height, setHeight] = useState(100);
  const divRef = React.createRef();

  // 使用 useEffect(异步执行,在浏览器绘制后)
  useEffect(() => {
    console.log('useEffect: 测量高度前 -', divRef.current?.offsetHeight);
    setHeight(divRef.current?.offsetHeight + 50);
    console.log('useEffect: 设置新高度 -', height);
  }, []);

  // 使用 useLayoutEffect(同步执行,在浏览器绘制前)
  useLayoutEffect(() => {
    console.log('useLayoutEffect: 测量高度前 -', divRef.current?.offsetHeight);
    setHeight(divRef.current?.offsetHeight + 50);
    console.log('useLayoutEffect: 设置新高度 -', height);
  }, []);

  return (
    <div ref={divRef} style={{ height, backgroundColor: 'lightblue' }}>
      当前高度: {height}px
    </div>
  );
}

export default LayoutEffectDemo;

当我们需要在 DOM 更新后立即进行一些操作,并且这些操作会影响到页面的布局时,就应该使用useLayoutEffect。比如测量 DOM 元素的尺寸并根据尺寸进行布局调整,使用它可以避免页面出现闪烁。

注意:由于是同步执行,过度使用useLayoutEffect可能会影响性能,所以在大多数情况下,应该优先使用useEffect

4. useReducer:复杂状态的管理者

当组件的状态逻辑比较复杂时,useState就显得有些力不从心了,这时useReducer就该登场了。

基本用法

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

其中,reducer是一个处理状态更新的函数,initialState是初始状态,state是当前状态,dispatch是用于触发状态更新的函数。

reducer 函数格式

const reducer = (state, action) => {​
  // 根据action.type处理不同的状态更新​
  switch (action.type) {​
    case 'TYPE1':​
      return newState1;​
    case 'TYPE2':​
      return newState2;​
    default:​
      return state;​
  }​
};

重要规则

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

  2. reducer函数应该是纯函数,即相同的输入始终返回相同的输出,不产生副作用。

使用技巧:可以结合immer库使用,简化状态更新的写法,不必手动复制原状态。

使用场景

  • 当状态逻辑复杂,包含多个子值时

  • 当状态更新依赖于之前的状态时

  • 当不同的操作需要修改同一状态时

示例

// 定义 reducer 函数
const counterReducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: action.payload }; // payload 是可选的携带数据
    default:
      throw new Error('未知 action');
  }
};

// 在组件中使用
function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+1</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
      <button onClick={() => dispatch({ type: 'reset', payload: 0 })}>
        Reset
      </button>
    </div>
  );
}

5. useRef:DOM 元素的抓手

useRef主要有两个用途:获取 DOM 结构和存储不需要触发重新渲染的值。

基本用法

const refContainer = useRef(initialValue);

获取 DOM 结构

通过将refContainer赋值给元素的ref属性,我们可以轻松获取 DOM 元素:

const inputRef = useRef(null);


// 在JSX中
<input ref={inputRef} />

// 之后就可以通过inputRef.current访问该DOM元素
inputRef.current.focus(); // 让输入框获得焦点

存储值

useRef创建的对象有一个current属性,我们可以用它来存储任何值,而且修改这个值不会触发组件重新渲染:

const timerRef = useRef(null);

// 存储定时器ID

timerRef.current = setInterval(() => {
// 执行操作
}, 1000);

// 清除定时器
clearInterval(timerRef.current);

6. useContext:跨组件数据传递的桥梁

在 React 应用中,组件嵌套是很常见的情况,当我们需要在跨多层的组件之间传递数据时,useContext就派上用场了。

使用步骤

  1. 创建 Context:
const MyContext = createContext(defaultValue);
  1. 提供 Context 值:

    使用MyContext.Provider组件包裹需要接收数据的组件树,并通过value属性提供数据:

<MyContext.Provider value={data}>
 // 子组件树
</MyContext.Provider>
  1. 使用 Context 值:

在需要使用数据的组件中,通过useContext获取:

const data = useContext(MyContext);

优点

  • 可以跨越多层组件传递数据,避免了 “props drilling”(props 层层传递)的问题

  • 当提供的value发生变化时,所有使用该 Context 的组件都会重新渲染

使用场景

  • 主题设置(如深色 / 浅色模式)

  • 用户登录状态

  • 多语言切换等需要全局共享的数据

总结

Hook核心功能语法应用场景注意事项
useState为函数组件添加状态管理能力,支持状态更新和重新渲染。const [state, setState] = useState(initialValue);简单状态管理(如计数器、表单值、开关状态)。1. 状态更新是异步的,多次调用可能合并。 2. 复杂状态(对象 / 数组)需返回新对象。
useEffect处理副作用(如数据获取、DOM 操作、订阅等),模拟类组件生命周期。useEffect(() => { /* 副作用代码 */ return () => { /* 清理函数 */ }; }, [dependencies]);1. 组件挂载 / 卸载时执行操作。 2. 依赖项变化时触发更新。1. 依赖项必须包含所有外部变量。 2. 清理函数在组件卸载前执行。
useLayoutEffect与 useEffect 类似,但在 DOM 更新后、浏览器绘制前同步执行,用于避免视觉闪烁。同 useEffect1. 需要立即读取 DOM 布局并同步更新。 2. 实现平滑动画过渡。同步执行可能阻塞渲染,优先使用 useEffect
useReducer复杂状态管理,将状态逻辑集中在 reducer 函数中,适合多子值或状态依赖场景。const [state, dispatch] = useReducer(reducer, initialState);1. 状态逻辑复杂(如购物车、表单校验)。 2. 下一个状态依赖于前一个状态。1. reducer 必须是纯函数,返回新状态。 2. 可结合 immer 简化不可变操作。
useRef创建可变的 ref 对象,存储不需要触发渲染的值(如 DOM 元素、定时器 ID)。const refContainer = useRef(initialValue);1. 获取 DOM 元素引用。 2. 存储上一次的值(如 prevProps)。修改 ref.current 不会触发组件重新渲染。
useContext跨组件层级共享状态,避免 props 层层传递。const value = useContext(MyContext);1. 全局状态(如主题、用户认证)。 2. 多层级组件间的数据传递。频繁使用可能导致组件耦合,建议结合状态管理库使用。