HOOKS

HOOKS

image

一、概要

:::
Hook 可以帮助在组件中使用不同的 React 功能。你可以使用内置的 Hook 或使用自定义 Hook。本页列出了 React 中常用 Hook。
:::

二、关键点

useState useRef useMemo useCallback useEffect useContext

二、内容

Hook 原则

  • 只在最顶层使用Hook;

  • 不要在循环,条件或嵌套函数中调用Hook;

  • 只在组件函数和自定义hook中调用Hook;

常用 Hook

useState

:::
useState 是一个 React Hook,它允许你向组件添加一个 状态变量。
:::

const [state, setState] = useState(initialState)

在组件顶层调用 useState 来声明一个状态变量。

import {useState} from "react";

export default function MyComponent () {

  const [count,setCount] = useState(0)
  
  return <div>
    状态值:{count}
    <button onClick={() => {
      setCount(origin => origin + 1)
    }}>加一</button>
  </div>
}

这里通过数组解构的方式来取 useState 的返回值,返回一个数组,第一个值是当前状态值,第二个是修改当前状态值的方法。

:::
注意:初始值 initialState 是可以接受任意值,但是对函数有特殊处理,被当做初始化函数执行将执行返回的结果作为初始值存储到 state 中,初次渲染后该函数将被忽略
:::

state: 当前状态变量

React 会将最新的状态变量渲染到页面上。

setState改变状态变量的方法

setState 函数,可以接受的入参为某个具体值,也可以是一个函数。

如果接受一个具体值:setState(value),会将该 value传递给 state,并且触发 React 的渲染。

如果接受一个函数:setState(origin => origin + 1),该origin => origin + 1将被执行,将返回值传递给 state,这个函数的入参是当前状态值,返回值是下一个状态值。

常见问题:

  1. 为什么拿不到最新的状态值?
import {useState} from "react";

export default function MyComponent () {

  const [count,setCount] = useState(0)
  
  return <div>
    状态值:{count}
    <button onClick={() => {
      setCount(origin => origin + 1)
      console.log(count)
    }}>加一</button>
  </div>
}

这里如果在第一次点击加一按钮后,期待打印的是 1,但是这个时候打印的是0,这是因为状态表象是一个快照,更新状态会请求另一个渲染,但不会影响你当前的 js 执行中的 count 值,所以如果你要获取最新值,你应该这样写:

import {useState} from "react";

export default function MyComponent () {

  const [count,setCount] = useState(0)
  
  return <div>
    状态值:{count}
    <button onClick={() => {
      const nextValue = count + 1
      setCount(nextValue)
      console.log(nextValue)
    }}>加一</button>
  </div>
} 
  1. 为什么修改了状态没有触发渲染
obj.x = 10;  // 🚩 错误:直接修改现有的对象
setObj(obj); // 🚩 不会发生任何事情

React 使用 Object.is 进行比较

const a = {value: 1}
const b = a;
Object.is(a,b); // true
b.value = 2;
Object.is(a,b); //true

const b = {value: 2}
Object.is(a,b); // false

所以应该给 setObj 传递一个新的值

setObj({
  ...obj,
  x: 10
})

useRef

:::
useRef 是一个可以帮助引用不需要渲染的值
:::

在组件顶层调用 useRef 以声明一个 ref

   const ref = useRef(initialState)

参数:

  • initialValue:ref 对象的 current 属性的初始值。可以是任意类型的值。这个参数在首次渲染后被忽略。

返回值:

useRef 返回一个只有一个属性的对象:

  • current:初始值为传递的 initialValue。之后可以将其设置为其他值。如果将 ref 对象作为一个 JSX 节点的 ref 属性传递给 React,React 将为它设置 current 属性。

在后续的渲染中,useRef 将返回同一个对象。

注意:

:::
改变 ref 不会触发重新渲染
:::

  • 可以修改 ref.current 属性。与 state 不同,它是可变的。然而,如果它持有一个用于渲染的对象(例如 state 的一部分),那么就不应该修改这个对象。

  • 改变 ref.current 属性时,React 不会重新渲染组件。React 不知道它何时会发生改变,因为 ref 是一个普通的 JavaScript 对象。

  • 除了初始化外不要在渲染期间写入或者读取 ref.current,否则会使组件行为变得不可预测。

使用:

  1. 接受一个值
import {useRef} from "react";

export default function MyComponent () {

  const ref = useRef<number>(0)
  
  return <div>
    ref值:{ref.current}
  </div>
}
  1. 接受一个 DOM
import {useRef} from "react";

export default function MyComponent () {

  const divRef = useRef<HTMLDivElement>(null)
  
  return <div ref={divRef}>
      <button
        onClick={() => {
          console.log(divRef.current?.clientHeight);
        }}
        >获取div的高度</button>
  </div>
}

:::
注意:自定义组件默认是没有 ref 属性的,需要通过 forwardRef 这个高阶组件来包裹
:::

  1. 避免重复

React 会保存 ref 初始值,并在后续的渲染中忽略它。

 function Video() {
      const playerRef = useRef(new VideoPlayer());
      // ...

虽然 new VideoPlayer() 的结果只会在首次渲染时使用,但是依然在每次渲染时都在调用这个方法。如果是创建昂贵的对象,这可能是一种浪费。

为了解决这个问题,你可以像这样初始化 ref:

function Video() {
  const playerRef = useRef(null);
  if (playerRef.current === null) {
    playerRef.current = new VideoPlayer();
  }
  // ...

useEffect

:::
用来做同步操作,简单点说,这个 hook 有两个机制,一个是主动触发,一个是被动触发,先看如何声明和参数信息
:::

useEffect(setup, dependencies?)

在组件的顶层调用 useEffect 来声明一个 Effect:

import { useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [serverUrl, roomId]);
  // ...
}

参数 :

  • setup:处理 Effect 的函数。setup 函数选择性返回一个 清理(cleanup) 函数。当组件被添加到 DOM 的时候,React 将运行 setup 函数。在每次依赖项变更重新渲染后,React 将首先使用旧值运行 cleanup 函数(如果你提供了该函数),然后使用新值运行 setup 函数。在组件从 DOM 中移除后,React 将最后一次运行 cleanup 函数。

  • 可选 dependenciessetup 代码中引用的所有响应式值的列表。响应式值包括 props、state 以及所有直接在组件内部声明的变量和函数。如果你的代码检查工具 配置了 React,那么它将验证是否每个响应式值都被正确地指定为一个依赖项。依赖项列表的元素数量必须是固定的,并且必须像 [dep1, dep2, dep3] 这样内联编写。React 将使用Object.is 来比较每个依赖项和它先前的值。如果省略此参数,则在每次重新渲染组件之后,将重新运行 Effect 函数。如果你想了解更多,请参见 传递依赖数组、空数组和不传递依赖项之间的区别。

返回值 :

useEffect 返回 undefined

注意事项 :

  • useEffect 是一个 Hook,因此只能在 组件的顶层 或自己的 Hook 中调用它,而不能在循环或者条件内部调用。如果需要,抽离出一个新组件并将 state 移入其中。

基于上述的案例,先说主动触发,主动触发在 ChatRoom 组件第一次渲染时,无论 roomId 和 serverUrl 为何值,内部函数都会被执行,当 roomId 或者 serverUrl 发生变更时,内部函数也会被触发。

useEffect 中第二个依赖参数有三种情况,

传递依赖项数组:

如果指定了依赖项,则 Effect 在 初始渲染后以及依赖项变更的重新渲染后 运行。

传递空数组:

如果你的 Effect 确实没有使用任何响应式值,则它仅在 初始渲染后 运行。

不传递:

如果完全不传递依赖数组,则 Effect 会在组件的 每次单独渲染(和重新渲染)之后 运行。

useCallback

:::
useCallback 是一个允许你在多次渲染中缓存函数的 React Hook。主要用于优化
:::

接受两个参数:

useCallback(fn, dependencies)

参数:

  • fn:想要缓存的函数。此函数可以接受任何参数并且返回任何值。React 将会在初次渲染而非调用时返回该函数。当进行下一次渲染时,如果 dependencies 相比于上一次渲染时没有改变,那么 React 将会返回相同的函数。否则,React 将返回在最新一次渲染中传入的函数,并且将其缓存以便之后使用。React 不会调用此函数,而是返回此函数。你可以自己决定何时调用以及是否调用。

  • dependencies:有关是否更新 fn 的所有响应式值的一个列表。响应式值包括 props、state,和所有在你组件内部直接声明的变量和函数。如果你的代码检查工具 配置了 React,那么它将校验每一个正确指定为依赖的响应式值。依赖列表必须具有确切数量的项,并且必须像 [dep1, dep2, dep3] 这样编写。

返回值:

在初次渲染时,useCallback 返回你已经传入的 fn 函数

在之后的渲染中, 如果依赖没有改变,useCallback 返回上一次渲染中缓存的 fn 函数;否则返回这一次渲染传入的 fn

为什么要用 useCallback?

假设你正在从 ProductPage 传递一个 handleSubmit 函数到 ShippingForm 组件中:

 function ProductPage({ productId, referrer, theme }) {
      // ...
      const handleSubmit = () => {
        // ...
      }
      
      return (
        <div className={theme}>
          <ShippingForm onSubmit={handleSubmit} />
        </div>
      );

默认情况下,当一个组件重新渲染时, React 将递归渲染它的所有子组件,因此每当因 theme 更改时而 ProductPage 组件重新渲染时,ShippingForm 组件也会重新渲染。这对于不需要大量计算去重新渲染的组件来说影响很小。这里涉及一个知识点就是在 JavaScript 中,**function () {}** 或者 **() => {}** 总是会生成不同的函数,所以每次渲染后,对于ShippingForm组件来说,他接受的 onSubmit 作为 props 传递的属性来说每次结果都是变化的,如果对于内部将 onSubmit 作为依赖的一些 hook 来说,会频繁触发,而你不希望这样,就可以使用 useCallback 包裹 handleSubmit

function ProductPage({ productId, referrer, theme }) {
  // ...
  const handleSubmit = useCallback(() => {
    // ...
  },[])
  
  return (
    <div className={theme}>
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );

这样,使用 Object.is 来比较这个 onSubmit 来说,就不会判断为更改

:::
**useCallback** 只应作用于性能优化。如果代码在没有它的情况下无法运行,请找到根本问题并首先修复它,然后再使用 useCallback
:::

useMemo

:::
useMemo 是一个 React Hook,它在每次重新渲染的时候能够缓存计算的结果。主要用于优化
:::

接受两个参数:

useMemo(calculateValue, dependencies)

参数 :

  • calculateValue:要缓存计算值的函数。它应该是一个没有任何参数的纯函数,并且可以返回任意类型。React 将会在首次渲染时调用该函数;在之后的渲染中,如果 dependencies 没有发生变化,React 将直接返回相同值。否则,将会再次调用 calculateValue 并返回最新结果,然后缓存该结果以便下次重复使用。

  • dependencies:所有在 calculateValue 函数中使用的响应式变量组成的数组。响应式变量包括 props、state 和所有你直接在组件中定义的变量和函数。如果你在代码检查工具中 配置了 React,它将会确保每一个响应式数据都被正确地定义为依赖项。依赖项数组的长度必须是固定的并且必须写成 [dep1, dep2, dep3] 这种形式。React 使用 Object.is将每个依赖项与其之前的值进行比较。

返回值 :

在初次渲染时,useMemo 返回不带参数调用 calculateValue 的结果。

在接下来的渲染中,如果依赖项没有发生改变,它将返回上次缓存的值;否则将再次调用 calculateValue,并返回最新结果。

:::
你应该仅仅把 **useMemo** 作为性能优化的手段。如果没有它,你的代码就不能正常工作,那么请先找到潜在的问题并修复它。然后再添加 useMemo 以提高性能。
:::

useContext

:::
用于读取和订阅组件中的上下文
:::

这个 hook 相对比较复杂,需要时间和经验来吃透,可以先看下官方文档,大致了解概念和使用方式

三方应用: https://react.docschina.org/reference/react/useContext

三、小练习

  1. 下面这段代码点击两次count+按钮打印的分别是什么内容?
function MyComponent () {
  const [count,setCount] = useState(0);

  const getValue = useCallback(() => {
    console.log(count);
  },[]);
  
  return <div>
    {count}
    <button
      onClick={() => {
        setCount(origin => origin + 1);
        getValue()
      }}
      >count+</button>
  </div>
}
  1. 为什么我直接修改了 useState 返回值的第一个值,并没有触发渲染?

  2. 我将 useRef 的返回值作为 useEffect 的依赖项传递,当我修改 useRef 的值,会不会触发 useEffect 的被动执行?为什么?

  3. useCallback 和 useMemo 的共同点和不同点有哪些?

  4. 父子组件,兄弟组件之间数据传递有哪些方式?

  5. props 和 state 的关系是什么?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值