常用的 React Hooks

65 阅读7分钟

常用的 React Hooks

常用的内置 React Hooks 一共有 8 个,分别是:

  • useState(前面已学习)
  • useContext(前面已学习)
  • useReducer
  • useRef
  • forwardRef & useImperativeHandle
  • useEffect
  • useMemo
  • useCallback

一、useReducer

useReducer 接收一个 reducer 函数和初始状态作为参数,并返回当前状态以及一个 dispatch 函数。这个 Hook 特别适用于管理复杂或嵌套的状态对象,以及当状态之间的逻辑比较复杂或需要多个状态共同作用时。

工作原理

  • Reducer 函数:Reducer 函数是一个纯函数,它接收当前的状态和一个代表“动作”的对象(action)作为参数。基于action.type,它决定如何计算并返回新的状态。Reducer 函数不应该修改传入的状态,而是应该返回一个新的状态对象。
  • Dispatch 函数:通过 useReducer 返回的 dispatch 函数,可以发送一个 action 到 reducer 函数,从而触发状态的更新。这个过程是同步的,即 dispatch 函数被调用后,reducer 函数会立即执行,并返回新的状态。

示例

以下是一个使用 useReducer 管理简单计数器状态的示例:

import { useReducer } from "react";
​
// 定义初始状态  
const initialState = { count: 0 };  
​
// 定义 reducer 函数  
function countReducer(state, action) {
  switch (action.type) {
    case 'increment':  
    return { count: state.count + 1 };  
    case 'decrement':  
      return { count: state.count - 1 };  
    case 'reset':  
      return initialState;  
    default:  
      throw new Error(); 
  }
}
​
function App() {
  const [state, dispatch] = useReducer(countReducer, initialState)
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>  
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>  
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>  
    </>
  );
}
export default App;

与 useState 的对比

useReduceruseState 都可以用于状态管理,但它们在适用场景上有所不同:

  • useState 适用于简单的状态逻辑,它允许直接在组件内部更新状态。
  • useReducer 则适用于复杂的状态逻辑,特别是当状态之间的更新逻辑相互依赖时。通过使用 reducer 函数,可以将状态更新逻辑封装在一个地方,使代码更加清晰和可维护。

二、useRef

useRef 主要用于在函数组件中创建一个可以在组件的整个生命周期内保持不变的引用对象。这个引用对象的 .current 属性是可变的,可以用来存储任何值,包括 DOM 元素的引用或者普通的变量值。

它的主要用途有以下两个:

  • 访问 DOM 元素

    useRef 最常见的用途之一是获取 DOM 元素的引用。可以通过给元素设置 ref 属性,并将 useRef 返回的引用对象作为值传递给它。然后通过访问 .current 属性来访问这个 DOM 元素。这对于需要直接操作 DOM(如设置焦点、测量尺寸等)的场景非常有用。

    import { useRef } from "react";
    ​
    function App() {
      const inputEl = useRef(null) 
      const onButtonClick = () => {
          // current 指向已挂载到 DOM 上的文本输入元素  
          inputEl.current.focus();  
      }
      return (
        <>
         <input ref={inputEl} type="text" />
         <button onClick={onButtonClick}>Focus the input</button> 
        </>
      );
    }
    export default App;
    
  • 存储可变值

    除了访问 DOM 元素外,useRef 还可以用来存储任何不会引起组件重新渲染的可变值。这在处理定时器、动画、媒体播放等场景时特别有用,因为可以安全地更新这些值而不用担心触发不必要的渲染。

    function TimerComponent() {  
      const secondsElapsed = useRef(0);  
      useEffect(() => {  
        const intervalId = setInterval(() => {  
          // 更新 secondsElapsed.current 而不会触发组件重新渲染  
          secondsElapsed.current += 1;  
          console.log(`Seconds elapsed: ${secondsElapsed.current}`);  
        }, 1000);  
    ​
        // 组件卸载时清除定时器  
        return () => clearInterval(intervalId);  
      }, []);  
    ​
      return <div>Timer Component</div>;  
    }
    

三、forwardRef & useImperativeHandle

forwardRefuseImperativeHandle 是两个与 Refs 和组件通信相关的 Hooks,它们主要用于父组件需要直接访问子组件内部 DOM 节点或子组件中定义的函数时。

这两个 Hooks 一起使用,可以创建一个能够暴露给父组件特定 API 的封装组件。

import { forwardRef, useImperativeHandle, useRef } from "react";
​
const Child = forwardRef((props, ref) => {
  useImperativeHandle(ref, () => ({
     myFn: () => {
      console.log('子组件');
    }
  }))
  return (
    <div>子组件</div>
  );
})
​
function App() {
  const childRef = useRef(null) 
  const handleClick = () => {
    childRef.current.myFn();  
  }
  return (
    <>
      <Child ref={childRef}/>
      <button onClick={handleClick}>按钮</button>
    </>
  );
}
export default App;

四、useEffect

useEffect 用于在函数组件中执行副作用操作。这些操作可以是数据获取、设置订阅、手动更改 DOM 等,它们通常发生在组件渲染到屏幕之后。

在类组件中,有生命周期方法(如 componentDidMountcomponentDidUpdatecomponentWillUnmount)来处理副作用,但在函数组件中,没有这些方法,所以使用 useEffect 来替代。

useEffect 接收两个参数:

  • 一个函数:这个函数包含了想执行的副作用操作。
  • 一个依赖项数组(可选) :这个数组中的值用于决定副作用函数何时应该重新执行。只有当数组中的值变化时,副作用函数才会再次执行。如果省略这个数组,副作用函数会在每次组件渲染后都执行。
import { useState, useEffect } from 'react';  
  
function MyComponent() {  
  const [data, setData] = useState(null);  
    
  // 使用 useEffect 获取数据  
  useEffect(() => {  
    // 这个函数会在组件挂载后执行  
    fetch('https://api.example.com/data')  
      .then(response => response.json())  
      .then(json => setData(json));  
  
    // 可选:返回一个清理函数,组件卸载时执行  
    return () => {  
      console.log('组件卸载');  
      // 在这里可以执行清理操作,比如取消订阅等  
    };  
  }, []); // 依赖项数组为空,意味着副作用只在组件挂载时执行一次  
  
  return (  
    <div>  
      {data ? <p>{data.message}</p> : <p>Loading...</p>}  
    </div>  
  );  
}  
  
export default MyComponent;

在这个例子中,useEffect 用于在组件挂载后从 API 获取数据,并在数据获取成功后更新组件的状态。由于依赖项数组为空,所以副作用函数只在组件首次渲染到 DOM 后执行一次。如果依赖项数组中有值,并且这些值在组件的后续渲染中发生了变化,那么副作用函数会再次执行。

五、useMemo

useMemo 类似于 Vue 中的 computed,主要用于缓存数据。它接收两个参数:一个计算函数和一个依赖项数组。

  • 计算函数:这是想要缓存其返回值的函数。
  • 依赖项数组:这是一个包含所有在计算函数中使用的响应式变量的数组。当这些依赖项发生变化时,useMemo 会重新调用计算函数并缓存新的返回值。
import { useMemo, useState } from "react";
​
function App() {
  const [num, setNum] = useState(5)
  const result = useMemo(() => {
    return num * 2
  }, [num])
​
  const handleClick = () => {
    setNum(num + 1)
  }
  return (
    <>
      <div>{result}</div>
      <button onClick={handleClick}>按钮</button>
    </>
  );
}
export default App;

六、useCallback

useCallback 可以缓存一个函数,以避免在组件的每次渲染中都重新创建该函数。

在 React 中,如果父组件将函数作为 prop 传递给子组件,并且这个函数在每次父组件渲染时都重新创建(比如直接定义在父组件的函数体内),那么即使这个函数的内容没有变化,子组件也会因为接收到一个新的函数引用而可能触发不必要的渲染。为了优化这种情况,可以使用 useCallback 来确保函数在依赖项没有变化时保持不变。

useCallback 接收两个参数:一个回调函数和一个依赖项数组。它返回一个新的回调函数,这个回调函数在依赖项没有变化时会保持与上一次渲染时相同。如果依赖项中的任何值发生变化,useCallback 会重新创建回调函数。

假设有一个按钮,每次点击时都会增加一个计数器的值。将这个函数传递给子组件,但不希望子组件因为接收到新的函数引用而每次都渲染。

不使用 useCallback 的代码:

function ParentComponent() {  
  const [count, setCount] = useState(0);  
  // 没有使用 useCallback,每次渲染都会创建一个新的 increment 函数  
  const increment = () => {  
    setCount(count + 1);  
  };  
  
  return (  
    <div>  
      <ChildComponent increment={increment} />  
      <p>Count: {count}</p>  
    </div>  
  );  
}

每次 ParentComponent 渲染时,increment 函数都会被重新创建,因为它是在渲染方法内部定义的。

使用 useCallback 的代码:

function ParentComponent() {  
  const [count, setCount] = useState(0);  
  
  // 使用 useCallback 来缓存 increment 函数  
  // 注意:这里依赖项数组是空的,因为没有在 increment 函数内部直接使用 count  
  const increment = useCallback(() => {  
    setCount(count => count + 1); // 使用函数式更新来避免直接依赖 count 的值  
  }, []);  
  
  return (  
    <div>  
      <ChildComponent increment={increment} />  
      <p>Count: {count}</p>  
    </div>  
  );  
}

由于 increment 函数并没有直接使用 count 的值(而是使用了setCount的函数式更新),所以依赖项数组应该是空的。因为依赖项数组为空,所以 increment 函数只会在组件首次渲染时创建一次,并在后续的渲染中保持不变。