6. React Hooks 一堆钩子,使得 函数组件 更强大!

4 阅读7分钟

React Hooks 一堆钩子,使得 函数组件 更强大!

React Hooks 是 React 16.8 引入的功能,它允许我们在 函数组件 中使用 类组件 的一些功能,如 state生命周期方法

在此之前,函数组件 仅仅是一个 接收 props返回 UI 的普通 JavaScript 函数,但随着 React Hooks 的引入,函数组件 变得更强大,能够处理 状态管理生命周期 等功能。

以下是 React 常用的 Hooks 及其用法:

1 useState - 管理组件的状态

useState 是最常用的 Hook,用于在 函数组件 中添加 stateuseState 会返回一个 数组,其中包括当前的 状态值 和 更新状态的 函数

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);  // count 是状态值,setCount 是更新该值的函数

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
  • useState(0):定义一个 count 状态初始值0
  • setCount:是一个 更新 count 状态的 函数,每次点击按钮时,count 的值会增加。

对象或数组 作为 state

const [state, setState] = useState({ count: 0, name: 'Alice' });

const updateCount = () => {
  setState(prevState => ({
    ...prevState,    // 保留之前的状态
    count: prevState.count + 1
  }));
};

延迟初始化状态

useState 可以接收一个 函数 作为 初始值,React 会 延迟计算 该值,直到需要时才执行。

const [count, setCount] = useState(() => computeInitialState());

2 useEffect - 管理副作用

useEffect 用于 执行 副作用 操作,例如 数据获取订阅手动 DOM 操作清理操作

它类似于 类组件 中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 生命周期方法。

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

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => setSeconds(seconds => seconds + 1), 1000);
    
    // Cleanup function: 在组件卸载时清理副作用
    return () => clearInterval(timer);
  }, []);  // 空依赖数组,表示仅在组件挂载时运行一次

  return <h1>Time: {seconds}</h1>;
}
  • useEffect 会在 组件 每次渲染后 执行,默认情况下,它在每次渲染后执行(类似于 componentDidUpdate);
  • 如果传递一个 空数组 [] 作为 第二个参数useEffect 只会在 组件挂载时 执行一次(类似于 componentDidMount);
  • useEffect返回值 是一个 清理函数在组件卸载时调用,类似于 componentWillUnmount

useEffect 的依赖项(第二个参数)

你可以将 useEffect 的 第二个参数 设置为 依赖项 数组,这样只有当 依赖项数组 发生变化时,useEffect 才会 重新执行

useEffect(() => {
  console.log('name changed:', name);
}, [name]);  // 只有 name 发生变化时才会重新执行

3 useContext - 使用上下文

useContextReact 提供的 Hook,用于在 函数组件 中使用 React Context,它能让你 访问到 祖先组件 传递下来的 数据

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

// 创建一个 Context 对象
const MyContext = createContext();

function Parent() {
  return (
    <MyContext.Provider value="Hello from context">
      <Child />
    </MyContext.Provider>
  );
}

function Child() {
  const contextValue = useContext(MyContext);  // 使用 useContext 获取上下文的值
  return <h1>{contextValue}</h1>;
}
  • MyContext.Provider 用于 提供 上下文的
  • useContext(MyContext) 用于在 Child 组件中 获取 传递的

4 useRef - 获取 DOM 元素 或 保存值

useRef 用于 获取 DOM 元素的引用,或者 保持一个 不会触发 重新渲染可变 值。

获取 DOM 元素引用

import React, { useRef } from 'react';

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

  const focusInput = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus the input</button>
    </div>
  );
}
  • useRef 返回一个 可变的 引用对象,该对象的 current 属性 指向 DOM 元素
  • inputRef.current 就是对 <input> 元素的 引用,调用 focus() 可以让该输入框获取焦点。

保持一个不触发渲染的值

React 的 useState 用于 管理状态,并且每当我们调用 setState 更新状态时,React 会重新渲染组件。但是 useRef 用来 保存某些值,这些值可以在组件的多次渲染之间 持续存在,且更新 useRef 的值 不会触发组件的重新渲染

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

function Timer() {
  const [seconds, setSeconds] = useState(0);  // 每次点击时更新秒数
  const prevTimeRef = useRef();  // 用来保存上次点击时的时间戳

  // 每次点击按钮时都记录当前时间,并与上次的时间戳对比
  const handleClick = () => {
    const currentTime = Date.now();  // 获取当前时间戳
    if (prevTimeRef.current) {
      const timeDifference = (currentTime - prevTimeRef.current) / 1000;  // 计算时间差,单位为秒
      console.log(`Time difference: ${timeDifference} seconds`);
    }
    prevTimeRef.current = currentTime;  // 更新上次点击的时间戳

    setSeconds(seconds + 1);  // 更新秒数
  };

  return (
    <div>
      <h1>Timer: {seconds} seconds</h1>
      <button onClick={handleClick}>Increment Time</button>
    </div>
  );
}

export default Timer;
为什么不使用 useState

如果我们使用 useState 来保存上次点击的时间戳,每次 时间戳更新 时,组件就会 重新渲染,这显然是没有必要的,因为我们只关心 seconds(显示的秒数),而 时间戳只需要保持在内存中不需要影响 UI

为什么会重新渲染?

React 的核心理念之一是 声明式编程:你声明 组件的 UI 如何基于 某些状态值显示,React 会负责根据 状态值的变化 自动更新 UI。因此,任何通过 useState 更新的状态 都会触发渲染,以便确保 UI 的状态最新的变量 同步

如果你 更新的状态 与 当前状态 相同(即状态没有发生变化),React跳过这次渲染,从而 提高性能。这是 React 的 “批量更新” 和 “浅比较” 优化的一个方面。

为什么不直接使用普通变量保存时间戳

在 React 中,组件的状态和渲染 是高度关联的。每次 组件重新渲染时,组件中的 所有局部变量 都会被 重新创建。换句话说,局部变量的值会丢失,而 useRef 是专门用于解决这个问题的。

5 useMemouseCallback - 性能优化

useMemouseCallback,这两个 Hook 用于 优化性能,避免 不必要的计算函数重新创建

useMemo- 缓存”昂贵“计算结果

useMemo 是 React 中的一个 优化工具,它用于 缓存计算结果,从而避免 每次组件重新渲染时 都进行 重复的计算,提升性能。

  • 避免不必要的计算:如果某些 计算非常耗时,且计算结果 依赖于 某些参数(状态),useMemo 可以缓存这些结果,避免每次渲染时(其它状态变化)都重新计算;
  • 提高性能:在组件渲染过程中,如果存在 重复的 昂贵计算useMemo 可以提高性能,减少不必要的计算开销。
import React, { useState, useMemo } from 'react';

function ItemList() {
  const [filter, setFilter] = useState('');
  const items = [
    { id: 1, name: 'Apple' },
    { id: 2, name: 'Banana' },
    { id: 3, name: 'Orange' },
    { id: 4, name: 'Grape' },
  ];

  // 使用 useMemo 缓存过滤后的结果,只有 filter 改变时才会重新计算
  const filteredItems = useMemo(() => {
    console.log('Filtering items...');
    return items.filter(item => item.name.toLowerCase().includes(filter.toLowerCase()));
  }, [filter]); // 依赖于 filter,只有 filter 变化时才重新计算

  return (
    <div>
      <input
        type="text"
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="Search items"
      />
      <ul>
        {filteredItems.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

export default ItemList;
  • 在输入框输入内容时,每次 filter 改变时,Filtering items... 会打印到控制台;
  • 如果你在没有改变 filter 的情况下多次渲染(例如 修改其他状态 或 重新渲染整个组件),useMemo返回之前缓存的结果,并不会重新执行 filterItems 函数。

useCallback - 缓存回调函数

useCallback 是 React 中的一个 Hook,用于 缓存回调函数,从而避免 在每次 组件渲染时重新创建 新的函数实例

在 React 中,每次组件渲染 都会 重新创建函数实例。这在多数情况下不会有问题,但如果这些函数 被传递到 子组件 作为 props,或者作为 某些依赖项(例如 useEffectuseMemo)的依赖,就可能导致 不必要的 重新渲染 和 性能问题。

useCallback 通过返回一个 缓存的函数 来避免这种情况,从而提高性能,特别是在 传递回调函数 给 子组件时。

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

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

  // 使用 useCallback 缓存 handleClick 函数
  const handleClick = useCallback(() => {
    setCount(count + 1);
  }, [count]); // 只有当 count 改变时,handleClick 才会重新创建

  return (
    <div>
      <h1>Count: {count}</h1>
      {/* 传递缓存的 handleClick 函数 */}
      <Child onClick={handleClick} />
    </div>
  );
}

function Child({ onClick }) {
  console.log('Child re-rendered!');
  return <button onClick={onClick}>Increment</button>;
}

export default Parent;
  • useCallback 会缓存 handleClick 函数,只有当 count 发生变化时handleClick 才会 重新创建。否则,React 会返回 上一次缓存的 函数实例
  • Child 组件 只有在 handleClick 函数的引用 发生变化时 才会 重新渲染,而不是每次 Parent 渲染时 都重新渲染。

6 useReducer - 复杂状态管理

useReduceruseState 的一个 替代方案,适用于 状态变化 比较复杂的 场景,特别是涉及 多个子值更新

import React, { useReducer } from 'react';

const initialState = { count: 0 };

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

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

  return (
    <div>
      <h1>Count: {state.count}</h1>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
    </div>
  );
}

useReducer 用于处理 复杂的 状态逻辑,接受一个 reducer 函数初始状态dispatch 用来触发 reducer 中定义的 状态更新 逻辑。