# React Hooks

# 基本概念

# 为什么需要 Hook

Hook 是 React 16.8 的新增特性,它可以让我们在不编写 class 的情况下使用 state 以及其他的 React 特性(比如生命周期)

我们先来思考一下 class 组件相对于函数式组件有什么优势?比较常见的是下面的优势:

  • class 组件可以定义自己的 state,用来保存组件自己内部的状态;
    • 函数式组件不可以,因为函数每次调用都会产生新的临时变量;
  • class 组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑;
    • 比如在 componentDidMount 中发送网络请求,并且该生命周期函数只会执行一次;
    • 函数式组件在学习 hooks 之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求;
  • class 组件可以在状态改变时只会重新执行 render 函数以及我们希望重新调用的生命周期函数 componentDidUpdate 等;
    • 函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次;

所以,在 Hook 出现之前,对于上面这些情况我们通常都会编写 class 组件。

# Class 组件存在的问题

  • 复杂组件变得难以理解:
    • 我们在最初编写一个 class 组件时,往往逻辑比较简单,并不会非常复杂。但是随着业务的增多,我们的 class 组件会变得越来越复杂;
    • 比如 componentDidMount 中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在 componentWillUnmount 中移除);
    • 而对于这样的 class 实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度;
  • 难以理解的 class:
    • 很多人发现学习 ES6 的 class 是学习 React 的一个障碍。
    • 比如在 class 中,我们必须搞清楚 this 的指向到底是谁,所以需要花很多的精力去学习 this;
    • 虽然我认为前端开发人员必须掌握 this,但是依然处理起来非常麻烦;
  • 组件复用状态很难:
    • 在前面为了一些状态的复用我们需要通过高阶组件或 render props;
    • 像我们之前学习的 redux 中 connect 或者 react-router 中的 withRouter,这些高阶组件设计的目的就是为了状态的复用;
    • 或者类似于 Provider、Consumer 来共享一些状态,但是多次使用 Consumer 时,我们的代码就会存在很多嵌套;
    • 这些代码让我们不管是编写和设计上来说,都变得非常困难;

# Hook 的出现

  • Hook 的出现,可以解决上面提到的这些问题;
  • 简单总结一下 hooks:
    • 它可以让我们在不编写 class 的情况下使用 state 以及其他的 React 特性;
    • 但是我们可以由此延伸出非常多的用法,来让我们前面所提到的问题得到解决;
  • Hook 的使用场景:
    • Hook 的出现基本可以代替我们之前所有使用 class 组件的地方(除了一些非常不常用的场景);
    • 但是如果是一个旧的项目,你并不需要直接将所有的代码重构为 Hooks,因为它完全向下兼容,你可以渐进式的来使用它;
    • Hook 只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用;

# useState 解析

  • useState 来自 react,需要从 react 中导入,它是一个 hook; 参数:初始化值,如果不设置为 undefined;
  • 返回值:数组,包含两个元素;
    • 元素一:当前状态的值(第一调用为初始化值);
    • 元素二:设置状态值的函数;
  • Hook 就是 JavaScript 函数,这个函数可以帮助你 钩入(hook into) React State 以及生命周期等特性;
  • 但是使用它们会有两个额外的规则:
    • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
    • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。

# 为什么叫 useState 而不叫 createState

“Create” 可能不是很准确,因为 state 只在组件首次渲染的时候被创建。在下一次重新渲染时,useState 返回给我们当前的 state。如果每次都创建新的变量,它就不是 “state”了。这也是 Hook 的名字 总是 以 use 开头的一个原因。

# 基本使用

Visual Studio Code 快速使用 useState:useState->选择代码段->回车

import React, { useState } from "react";

export default function MulutState() {
  const [变量名, set变量名] = useState(初始值);
  const [变量名, set变量名] = useState(["值", "值"]);

  return (
    <div>
      <h2>{变量名}</h2>
    </div>
  );
}

# 修改数据

// 修改普通数据
set变量名();

// 改变数组内Object的值
const Object = [...变量名];
Object[index].=;
set变量名(Object);

# 多个 useState 合并

当执行多个同一 useState 时,React 会将多个 state 进行合并

setCount(count + 10);
setCount(count + 10);
setCount(count + 10);
setCount(count + 10);

// count默认值为0,当执行以上代码后,count值为10

解决方案:

setCount((prevCount) => prevCount + 10);
setCount((prevCount) => prevCount + 10);
setCount((prevCount) => prevCount + 10);
setCount((prevCount) => prevCount + 10);

# Effect Hook

Effect Hook 可以让完成一些类似于 class 中生命周期的功能; 事实上,类似于网络请求、手动更新 DOM、一些事件的监听,都是 React 更新 DOM 的一些副作用(Side Effects); 所以对于完成这些功能的 Hook 被称之为 Effect Hook;

提示

useEffect 的第二个参数表示依赖的变量,只有当依赖的变量发生改变时才调用此 Hook。
如果不传值,则 DOM 重新渲染时会重新调用此 Hook。传递空数组则表示只执行一次

import React, { useEffect } from "react";

useEffect(() => {
  // 修改DOM
});

useEffect(() => {
  // 修改DOM(只有当依赖的变量发生改变时才调用此Hook)
}, [依赖变量1, 依赖变量2]);

useEffect(() => {
  // 订阅事件

  return () => {
    // 取消订阅事件
  };
}, []);

useEffect(() => {
  // 网络请求
}, []);

# 跨组件传参

import React, { createContext } from "react";

export const Context名 = createContext();

function App() {
  return (
    <div>
      <Context名.Provider value={{ 变量名:}}>
        <传参组件名></传参组件名>
      </Context名.Provider>
    </div>
  );
}
import React, { useContext } from "react";
import { Context名 } from "../App";

export default function 传参组件名() {
  const Context别名 = useContext(Context名);
  console.log(Context别名);

  return (
    <div>
      <h2>{Context别名.变量名}</h2>
    </div>
  );
}

# useReducer

useReducer 仅仅是 useState 的一种替代方案,并不能替代 Redux:

  • 在某些场景下,如果 state 的处理逻辑比较复杂,我们可以通过 useReducer 来对其进行拆分;
  • 或者这次修改的 state 需要依赖之前的 state 时,也可以使用;
  • 数据不会共享,它们只是使用了相同的 counterReducer 的函数而已。
import React, { useReducer } from "react";

function reducer(state, action) {
  switch (action.type) {
    case "类型名1":
      return { ...state, 变量名: state.变量名 + 1 };
    case "类型名2":
      return { ...state, 变量名: state.变量名 - 1 };
    default:
      return state;
  }
}

export default function Home() {
  const [state, dispatch] = useReducer(reducer, { 变量名: 默认值 });

  return (
    <div>
      <h2>{state.变量名}</h2>
      <button onClick={(e) => dispatch({ type: "类型名1" })}>+1</button>
      <button onClick={(e) => dispatch({ type: "类型名2" })}>-1</button>
    </div>
  );
}

# useCallback

useCallback 实际的目的是为了进行性能的优化:

  • useCallback 会返回一个函数的 memoized(记忆的) 值;
  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的;

提示

使用场景:在将一个组件中的函数,传递给子元素进行回调使用时,使用 useCallback 对函数进行处理

import React, { useState, useCallback, memo } from "react";

const HYButton = memo((props) => {
  console.log("HYButton 重新渲染");
  return <button onClick={props.incremt}>HYButton +1</button>;
});

export default function CallbackHookDemo01() {
  console.log("CallbackHookDemo01 重新渲染");

  const [count, setCount] = useState(0);
  const [show, setShow] = useState(true);

  const incremt1 = () => {
    console.log("执行incremt1函数");
    setCount(count + 1);
  };

  const incremt2 = useCallback(() => {
    console.log("执行incremt2函数");
    setCount(count + 1);
  }, [count]);

  return (
    <div>
      <h2>{count}</h2>
      <HYButton incremt={incremt1}></HYButton>
      <HYButton incremt={incremt2}></HYButton>
      <button onClick={(e) => setShow(!show)}>show切换</button>
    </div>
  );
}

# useMemo

useMemo 实际的目的也是为了进行性能的优化

  • useMemo 返回的也是一个 memoized(记忆的) 值;
  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的;
import React, { useState, useMemo } from "react";

function calcNumber(count) {
  console.log("重新计算");
  return ((1 + count) * count) / 2;
}

export default function MemoHookDemo01() {
  const [count, setCount] = useState(10);
  const [show, setShow] = useState(true);

  //  const total = calcNumber(count);

  const total = useMemo(() => {
    return calcNumber(count);
  }, [count]);

  return (
    <div>
      <h2>计算数字的和:{total}</h2>
      <button onClick={(e) => setCount(count + 1)}>+1</button>
      <button onClick={(e) => setShow(!show)}>show切换</button>
    </div>
  );
}

# useRef

useRef 返回一个 ref 对象,返回的 ref 对象再组件的整个生命周期保持不变。

最常用的 ref 是两种用法:

  1. 引入 DOM(或者组件,但是需要是 class 组件)元素;
  2. 保存一个数据,这个对象在整个生命周期中可以保存不变;

# useRef 引用 DOM

import React, { useRef } from "react";

export default function 组件名() {
  const Ref名 = useRef();

  return (
    <div>
      <h2 ref={Ref名}>RefHookDemo01</h2>
      <button onClick={(e) => changeDOM()}>修改DOM</button>
    </div>
  );

  function changeDOM() {
    console.log(Ref名.current);
    Ref名.current.innerHTML = "Hello World";
  }
}

# useRef 引用其他数据

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

export default function RefHookDemo02() {
  const [count, setCount] = useState(0);
  const numRef = useRef(count);

  useEffect(() => {
    numRef.current = count;
  }, [count]);

  return (
    <div>
      <h2>count 上一次的值:{numRef.current}</h2>
      <h2>count 这一次的值:{count}</h2>
      <button onClick={(e) => setCount(count + 10)}>+10</button>
    </div>
  );
}

# useImperativeHandle

通过 useImperativeHandle 可以值暴露固定的操作:

  • 通过 useImperativeHandle 的 Hook,将传入的 ref 和 useImperativeHandle 第二个参数返回的对象绑定到了一起;
  • 所以在父组件中,使用 inputRef.current 时,实际上使用的是返回的对象;
  • 比如我调用了 focus 函数,甚至可以调用 printHello 函数;
import React, { useRef, forwardRef, useImperativeHandle } from "react";

const HYInput = forwardRef((props, ref) => {
  const inputRef = useRef();

  useImperativeHandle(
    ref,
    () => ({
      focus: () => {
        inputRef.current.focus();
      },
    }),
    [inputRef]
  );

  return <input ref={inputRef} type="text" />;
});

export default function ForwardRefDemo() {
  const inputRef = useRef();

  return (
    <div>
      <HYInput ref={inputRef}></HYInput>
      <button onClick={(e) => inputRef.current.focus()}>聚焦</button>
    </div>
  );
}

# useLayoutEffect

useLayoutEffect 和 useEffect 非常的相似,事实上他们也只有一点区别

  • useEffect 会在渲染的内容更新到 DOM 上后执行,不会阻塞 DOM 的更新;
  • useLayoutEffect 会在渲染的内容更新到 DOM 上之前执行,会阻塞 DOM 的更新
  • 如果我们希望在某些操作发生之后再更新 DOM,那么应该将这个操作放到 useLayoutEffect。
import React, { useState, useEffect, useLayoutEffect } from "react";

export default function EffectCountDemo() {
  const [count, setCount] = useState(10);

  // 如使用useEffect,会先渲染0,然后再渲染随机数
  useLayoutEffect(() => {
    if (count === 0) {
      setCount(Math.random());
    }
  }, [count]);

  return (
    <div>
      <h2>数字:{count}</h2>
      <button onClick={(e) => setCount(0)}>修改数字</button>
    </div>
  );
}

# 自定义 Hook

自定义 Hook 本质上只是一种函数代码逻辑的抽取,严格意义上来说,它本身并不算 React 的特性

提示

在函数前加上use即可成为自定义 Hook

function useHook名() {
  // Hooks相关代码(如useEffect等)
}

# Context 共享

import { useContext } from "react";
import { UserContext, TokenContext } from "../App";

function useUserContext() {
  const user = useContext(UserContext);
  const token = useContext(TokenContext);
  return [user, token];
}

export default useUserContext;

使用

import React from "react";
import userUserContext from "../hooks/user-hook";

export default function CustomContextShareHook() {
  const [user, token] = userUserContext();

  console.log(user, token);

  return (
    <div>
      <h2>CustomContextShareHook</h2>
    </div>
  );
}

# 获取滚动位置

import { useEffect, useState } from "react";

function useScrollPosition() {
  const [scrollPosition, setScrollPosition] = useState(0);

  useEffect(() => {
    const handleScroll = () => {
      setScrollPosition(window.scrollY);
    };

    document.onscroll = handleScroll;

    return () => {
      document.onscroll = null;
    };
  }, []);

  return [scrollPosition];
}

export default useScrollPosition;

使用:

import React, { useEffect, useState } from "react";
import useScrollPosition from "../hooks/scroll-position-hook";

export default function CustomScrollPositionHook() {
  const scrollPosition = useScrollPosition();

  return (
    <div style={{ padding: "1000px 0" }}>
      <h2 style={{ position: "fixed", left: 0, top: 0 }}>
        CustomScrollPositionHook:{scrollPosition}
      </h2>
    </div>
  );
}

# localStorage 存储

import { useState, useEffect } from "react";

function useLocalStorage(key) {
  const [name, setName] = useState(() => {
    const name = JSON.parse(window.localStorage.getItem(key));
    return name;
  });

  useEffect(() => {
    window.localStorage.setItem(key, JSON.stringify(name));
  }, [name]);

  return [name, setName];
}

export default useLocalStorage;

使用:

import React, { useState, useEffect } from "react";
import useLocalStorage from "../hooks/local-store-hook";

export default function CustomDataStoreHook() {
  const [name, setName] = useLocalStorage();

  return (
    <div>
      <h2>CustomDataStoreHook: {name}</h2>
      <button onClick={(e) => setName("coderwhy")}>设置name</button>
    </div>
  );
}
最后更新: 2022/9/12 16:01:28