React Hooks详解

110 阅读8分钟

0 引入

React组件有两种形式:类组件、函数组件。在React 16.8 版本推出之前,类组件是主力军,而函数组件往往只用来向 UI 渲染简单组件。

从版本16.8.0开始,React引入了hooks。使用React hooks,我们可以在函数组件中使用state和生命周期方法。hooks是添加了state和生命周期的函数组件,所以有了hooks,函数组件和类组件没有什么区别。

但是,现在人们在写 React 组件时,使用React hooks的函数组件更常用,因为它们使代码更短,更容易理解。

1 使用React Hooks的规则

在使用 React Hooks 时,有几个规则需要遵守(你可以在学完了重要的hooks之后再回来理解这些规则):

  • 只在组件的顶层调用 Hooks:你不应该在循环、条件或嵌套函数中使用 Hooks。相反,总是在你的 React 函数的顶层使用 Hooks,在任何 return 关键字之前。
  • 只从React函数中调用Hooks:不要从普通的 JavaScript 函数中调用 Hooks。你可以:
    ✅ 从 React 函数组件中调用 Hooks
    ✅ 从自定义 Hooks 中调用 Hooks

2 常见React Hooks

到目前为止,React 有 10 个内置 Hooks。本文将介绍如下几个重要的hook:

  • useState
  • useEffect
  • useRef
  • useContext
  • useReducer

3 useState

useState Hook 允许你在函数组件内创建、更新和操作state。state允许我们管理应用程序中不断变化的数据。它被定义为一个对象,我们在其中定义键-值对,指定我们希望在应用程序中追踪的各种数据。

useState 接受一个参数,该参数作为state的初始值。你可以提供任何值作为初始值,如number, string, boolean, object, array, null等。

useState 返回一个数组,它的第一个值是当前state的值。第二个值是我们将用于更新state的函数。

import { useState } from "react";

function Counter() {
    const [count, setCount] = useState(0); return (
        <div>
        Current Cart Count: {count}
            <div>
            <button onClick={() => setCount(count - 1)}>Add to cart</button>
            <button onClick={() => setCount(count + 1)}>Remove from cart</button>
            </div>
        </div>
    );
}

在上面的代码中,我们做了一些事:

  • 首先从react包中使用对象解构引入useState
  • 由于useState返回一个数组,我们可以用数组结构获取返回值,上述代码中count的初始值为0,setCount是一个函数
  • button标签中,我们定义了一个事件,当button被点击时,会触发事件,即调用setCount(count - 1),React中规定,任何时候调用setCount函数,都会触发页面的重新渲染。

注意:useState的初始值只会在组件第一次渲染时生效。也就是说,之后触发的每次渲染,useState获取到的都是最新的而不是最初始的state值。

4 useEffect

useEffect Hook允许在函数组件中执行副作用(side effect)。

副作用是相对于主作用来说的,一个函数除了主作用,其他的作用就是副作用。对于 React 组件来说,主作用就是根据数据(state/props)渲染 UI,除此之外都是副作用(比如,手动修改 DOM、增加监听器等等)。

使用useEffect,必须提供一个函数作为参数。在组件第一次被渲染的时候,以及在随后的每次重新渲染/更新(如useState触发更新)时,React都会调用这个函数。React 首先更新 DOM,然后调用任何传递给 useEffect() 的函数。

const { useEffect, useState } = React

const CounterWithNameAndSideEffect = () => {
  const [count, setCount] = useState(0)
  useEffect(() => {
    console.log(`You clicked ${count} times`)
  })
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

在上面的代码中,我们做了一些事:

  • 同样的,我们首先在react包中引入useEffect
  • useEffect接收两个参数,其中第一个参数是必需的,并且必须是一个函数。当不使用第二个参数时,React将在第一次及后续每次渲染时调用这个函数。

4.1 useEffect的第二个参数

因为在随后的每次重新渲染 / 更新时,传递给 useEffect() 的函数都会被执行,所以出于性能上的考虑,我们可以告诉 React 在某些时候不要执行这个函数。为了实现这个目的,我们可以为 useEffect() 传入第二个参数,这个参数是一个数组,它的成员是需要监视的 state 变量。只有在这些 state 发生变化的时候,React 才会执行这个函数。

useEffect(() => {
  console.log(`Hi ${name} you clicked ${count} times`)
}, [name, count])

类似的,你可以传入一个空数组,这会使 React 只在组件挂载(即首次渲染)时执行这个函数。

useEffect(() => {
  console.log(`Component mounted`)
}, [])

4.2 useEffect清理功能

如果想要清理副作用 可以在useEffect的末尾return一个新的函数,在新的函数中编写清理副作用的逻辑。

清理函数可以防止内存泄漏,并删除一些不必要的和不需要的行为。

清理的执行时机为:

  1. 组件卸载时自动执行
  2. 组件更新时,下一个useEffect副作用函数执行之前自动执行
const UseEffectCleanup = () => {
  const [size, setSize] = useState(window.innerWidth);
  const checkSize = () => {
    setSize(window.innerWidth);
  };
  useEffect(() => {
    window.addEventListener('resize', checkSize);
    return () =>window.removeEventListener('resize'  , checkSize);
  }, []);return (
    <>
      <h2>{size} PX</h2>
    </>
  );
};

4.3 useEffect的应用

useEffect() 非常适合添加日志,访问第三方 API 等。

5 useRef

使用useRef函数,可以在不re-render的状态下更新值,它主要被用来获取DOM进而控制DOM的行为。

5.1 什么是useRef?

const [renderCounter,setRenderCount] = useState(0)
//renderCounter:0

const renderCounter = useRef(0)
//renderCounter: {current:0}

useState会返回一个包含值的数组,第一个值是state,第二个值是更新state的函数。每次更新、renderCount改变,就会触发re-render。

而useRef传递一个值并返回一个对象,这个值更新,不会触发re-render。

5.2 使用useRef获取DOM

原生JS中,可以使用getElementById、querySelector等方法获取DOM,通过在React中使用useRef,我们仍然能像过去一样,类似 ID 的方式获取 DOM 元素。

const UseRefBasics = () => {
  const refContainer = useRef(null); const handleSubmit= (e)=>{
    e.preventDefault();
   // console.log(refContainer);
    console.log(refContainer.current)
  }
  return(
      <>
        <form className='form' onSubmit={handleSubmit}>
          <div>
            <input type='text' ref={refContainer} />
          </div>
          <button type='submit'>submit</button>
        </form>
      </>
  );
};

在这段代码中,当点击提交时,触发handleSubmit函数,输出refContainer.current的值,为下面的DOM元素。

<input type='text' />

这样我们就通过 refContainer.current 获取到了当前的 DOM 元素了。

6 useContext

useContext Hook 与 React Context API 一起工作。它提供了一种方法,使整个应用程序中的组件通信变得简单,无论它们的嵌套有多深。

React 有一个单向的数据流,数据只能从父代传递到子代。要把数据(如 state)从父组件传给子组件,你需要根据子组件的嵌套深度,把它作为一个 prop,通过不同的级别向下传递。手动地在组件树上传递它们是很复杂的。

使用Context API包裹需要传值的组件,这个值会一直存在直到组件树的最底部。只需要三步:

step1. 使用React提供的createContext方法创建context,此方法返回两个组件Provider和Consumer。

const PersonContext = React.createContext();   //第一步,创建上下文,这会返回两个组件Provider和Consumer

step2. 用Provider组件包裹需要传值的组件(往往是根组件)

const ContextAPI = () => {
  const [people, setPeople] = useState(data);
  const removePerson = (id) => {
    setPeople((people) => {
      return people.filter((person) => person.id !== id);
    });
  };
  return (
    // 第二步,Provider包裹根组件,这里的value会all the way down
    <PersonContext.Provider value={{ people, removePerson }}>
      <h3>Context API/useContext</h3>
      <List />
    </PersonContext.Provider>
  );
};

step3. 在需要值的组件内部使用useContext钩子接收值(常用对象解构接收)

const List = () => {
  const { people } = useContext(PersonContext);
  return (
    <>
      {people.map((person) => {
        return <SinglePerson key={person.id} {...person} />;
      })}
    </>
  );
};

const SinglePerson = ({ id, name }) => {
  //第三步,在需要数据的组件处使用useContext
  const { removePerson } = useContext(PersonContext);
  return (
    <div className="item">
      <h4>{name}</h4>
      <button onClick={() => removePerson(id)}>remove</button>
    </div>
  );
};

7 useReducer

useReducer Hooks 是 useState Hooks 的一个替代品。可以实现复杂逻辑修改,而不是像useState那样只是直接赋值修改。对于复杂的state操作逻辑,嵌套的state的对象,官方推荐使用useReducer。

准确来说,useReducer是useState的原始版,因为在React源码中,useState就是由useReducer实现的。

7.1 useReducer基本用法

const [state, dispatch] = useReducer(reducer, initialState);
  • reducer:简单来说 reducer是一个函数(state, action) => newState:接收当前应用的state和触发的动作action,计算并返回最新的state
  • initialState:自定义状态初始值。
  • state:自定义状态值的引用
  • dispatch:用来触发reducer函数。

补充说明:

  1. 可以将reducer理解为一个事件处理函数,它定义了一套处理机制,dispatch根据这套机制进行调度。reducer本质是一个纯函数,没有任何UI和副作用。这意味着相同的输入(state、action),reducer函数无论执行多少遍始终会返回相同的输出(newState)

2. initialValue是我们自定义变量的默认值,该值可以是简单类型(number、string),也可以是复杂类型(object、array)。
即使该值是简单类型,也建议单独定义出来而不是直接将值写在useReducer函数中,因为单独定义可以让我们更加清晰读懂数据结构。

  1. dispatch function 通常以下列格式派发一个对象:
dispatch({ type: "ACTION_TYPE", payload: optionalArguments });

其中 type 是动作的描述,payload 是你要传递给 reducer 的参数。

 7.3 代码说明

我们通常会在组件外部定义reducer和initialValue,作为useReducer的两个参数传入。复杂的reducer也可以在文件外部定义,并在组件文件中导入。

注意:

  1. reducer处理的state对象必须是immutable,这意味着永远不要直接修改参数中的state对象,reducer函数应该每次都返回一个新的state object
  2. 既然reducer要求每次都返回一个新的对象,我们可以使用ES6中的解构赋值方式去创建一个新对象,并复写我们需要改变的state属性
  3. reducer是一个利用action提供的信息,将state从oldState转换到newState的一个纯函数。
import React, { useState, useReducer } from "react";
import Modal from "./Modal";
import { data } from "../../../data";

// 在外部单独定义初始状态
const defaultState = {
  people: [],
  isModalOpen: false,
  modalContent: "",
};
// 定义reducer事件处理函数,它规定了一套机制,当dispatch触发不同操作时,会根据reducer的规定return不同值
const reducer = (state, action) => {
  if (action.type === "ADD_ITEM") {
    const newPeople = [...state.people, action.payload];
    return {
      ...state,
      people: newPeople,
      isModalOpen: true,
      modalContent: "item added",
    };
  } else if (action.type === "NO_VALUE") {
    return {
      ...state,
      isModalOpen: true,
      modalContent: "please enter value",
    };
  }
};

在组件中使用useReducer钩子,它将返回两个变量,state是initialValue的引用,也就是我们定义的状态合集。dispatch相当于一个调度函数,在其中传入你想执行的操作,dispatch将会根据reducer中该名称返回对应newState。


// Index组件
const Index = () => {
  const [name, setName] = useState("");
  const [state, dispatch] = useReducer(reducer, defaultState);
  
  const handleSubmit = (e) => {
    e.preventDefault();
    if (name) {
      const newItem = { id: new Date().getTime().toString(), name };
      dispatch({ type: "ADD_ITEM", payload: newItem });
      setName("");
    } else {
      dispatch({ type: "NO_VALUE" });
    }
  };
  return (
    <>
      {state.isModalOpen ? <Modal modalContent={state.modalContent} /> : ""}
      <form className="form" onSubmit={handleSubmit}>
        <div>
          <input
            type="text"
            value={name}
            onChange={(e) => {
              setName(e.target.value);
            }}
          ></input>
        </div>
        <button type="submit">submit</button>
      </form>
      {state.people.map((person) => {
        return (
          <div key={person.id}>
            <h4>{person.name}</h4>
          </div>
        );
      })}
    </>
  );
};

export default Index;

 7.3 useReducer的应用

在React 16.8版本以前,通常需要使用第三方Redux来管理React的公共数据,但自从 React Hook 概念出现以后,可以使用 useContext + useReducer 轻松实现 Redux 相似功能。