ReactHook的使用和理解

136 阅读24分钟

九、ReactHook

认识和体验Hooks

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,但是依然处理起来非常麻烦;

组件复用状态很难

  • 在前面为了一些状态的复用我们需要通过高阶组件;
  • 像我们之前学习的redux中connect或者react-router中的withRouter,这些高阶组件设计的目的就是为了状态的复用;
  • 或者类似于Provider、Consumer来共享一些状态,但是多次使用Consumer时,我们的代码就会存在很多嵌套;
  • 这些代码让我们不管是编写和设计上来说,都变得非常困难;

Hook的出现

Hook的出现,可以解决上面提到的这些问题;

简单总结一下hooks:

  • 它可以让我们在不编写class的情况下使用state以及其他的React特性;
  • 但是我们可以由此延伸出非常多的用法,来让我们前面所提到的问题得到解决;

Hook的使用场景:

  • Hook的出现基本可以代替我们之前所有使用class组件的地方;
  • 但是如果是一个旧的项目,你并不需要直接将所有的代码重构为Hooks,因为它完全向下兼容,你可以渐进式的来使用它;
  • Hook只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用;

在我们继续之前,请记住 Hook 是:

  • 完全可选的:你无需重写任何已有代码就可以在一些组件中尝试 Hook。但是如果你不想,你不必现在就去学习或使用 Hook。
  • 100% 向后兼容的: Hook 不包含任何破坏性改动。
  • 现在可用:Hook 已发布于 v16.8.0。

image-20221116121828999

useState

State Hook的API就是 useState,我们在前面已经进行了学习:

  • useState会帮助我们定义一个 state变量,useState 是一种新方法,它与 class 里面的 this.state 提供的功能完全相同。

    • 一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。
  • useState接受唯一一个参数,在第一次组件被调用时使用来作为初始化值。(如果没有传递参数,那么初始化值为undefined)。

  • useState的返回值是一个数组,我们可以通过数组的解构,来完成赋值会非常方便。

FAQ:为什么叫 useState 而不叫 createState?

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

当然,我们也可以在一个组件中定义多个变量和复杂变量(数组、对象)

useState解析

const App = memo(() => {
  const [message, setMessage] = useState("Hello World");
​
  function changeMessage() {
    setMessage("你好 世界");
  }
​
  return (
    <div>
      <h2>App:{message}</h2>
      <button onClick={changeMessage}>修改文本</button>
    </div>
  );
});

那么我们来研究一下核心的一段代码代表什么意思:

  • useState来自react,需要从react中导入,它是一个hook;

    • 参数:初始化值,如果不设置为undefined;

    • 返回值:数组,包含两个元素;

      • 元素一:当前状态的值(第一调用为初始化值);
      • 元素二:设置状态值的函数;
  • 点击button按钮后,会完成两件事情:

    • 调用setCount,设置一个新的值;
    • 组件重新渲染,并且根据新的值返回DOM结构;

useStateHook原理

  • 在useStateHook中,React会在一个地方将状态保存下来
  • 当新的值改变状态时,会使组件重新渲染
  • 并且将新的值返回到原来的组件中

image-20221112172647943

◼ 但是使用它们会有两个额外的规则:

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
  • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。

useEffect

目前我们已经通过hook在函数式组件中定义state,那么类似于生命周期这些呢?

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

◼ 假如我们现在有一个需求:页面的title总是显示counter的数字,使用Hook实现:

import React, { memo, useState, useEffect } from "react";
​
const App = memo(() => {
  const [count, setCount] = useState(200);
​
  useEffect(() => {
    // 当前传入的回调函数会在组件被渲染完成后,自动执行
    // 网络请求/DOM操作(修改标签)/事件监听
    document.title = count;
  });
​
  return (
    <div>
      <h2>当前计数:{count}</h2>
      <button onClick={(e) => setCount(count + 1)}>+1</button>
    </div>
  );
});
​
export default App;

useEffect的解析:

  • 通过useEffect的Hook,可以告诉React需要在渲染后执行某些操作
  • useEffect要求我们传入一个回调函数,在React执行完更新DOM操作之后,就会回调这个函数
  • 默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个 回调函数

需要清除Effect

在class组件的编写过程中,某些副作用的代码,我们需要在componentWillUnmount中进行清除:

  • 比如我们之前的事件总线或Redux中手动调用subscribe;
  • 都需要在componentWillUnmount有对应的取消订阅;
  • Effect Hook通过什么方式来模拟componentWillUnmount呢?

◼ useEffect传入的回调函数A本身可以有一个返回值,这个返回值是另外一个回调函数B:

type EffectCallback = () => (void | (() => void | undefined));

为什么要在 effect 中返回一个函数?

  • 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数;
  • 如此可以将添加和移除订阅的逻辑放在一起;
  • 它们都属于 effect 的一部分;
import React, { memo, useEffect, useState } from "react";
​
const App = memo(() => {
  const [count, setCount] = useState(100);
  // 负责告知react,在执行玩当前组件渲染之后要执行的副作用代码
  useEffect(() => {
    // 1.监听事件
    // const unubscribe = store.subscribe(() => {});
    // function foo() {}
    // eventBus.on("why", foo);
    console.log("监听redux数据变化");
​
    // 返回值:回调函数 => 组件被重新渲染或者组件卸载的时候执行
    return () => {
      console.log("取消监听redux中数据变化");
    };
  });
  return (
    <div>
      <button onClick={(e) => setCount(count + 1)}>+1({count})</button>
    </div>
  );
});
​
export default App;

React 何时清除 effect?

  • React 会在组件更新和卸载的时候执行清除操作;
  • 正如之前学到的,effect 在每次渲染的时候都会执行;

使用多个Effect

使用Hook的其中一个目的就是解决class中生命周期经常将很多的逻辑放在一起的问题:

  • 比如网络请求、事件监听、手动修改DOM,这些往往都会放在componentDidMount中;

使用Effect Hook,我们可以将它们分离到不同的useEffect中:

Hook 允许我们按照代码的用途分离它们, 而不是像生命周期函数那样:

  • React 将按照 effect 声明的顺序依次调用组件中的每一个effect;
import React, { memo, useEffect, useState } from "react";
​
const App = memo(() => {
  const [count, setCount] = useState(100);
  // 负责告知react,在执行玩当前组件渲染之后要执行的副作用代码
  useEffect(() => {
    // 1.修改document的title
    console.log("修改title");
  });
​
  // 一个函数式组件中,可以存在多个useEffect
  useEffect(() => {
    // 2.对redux中数据变化监听
    console.log("对redux进行监听");
    return () => {
      // 取消redux中数据的监听
    };
  });
​
  useEffect(() => {
    // 3.监听eventBus中的why事件
    console.log("对eventBus中的why事件监听");
    return () => {
      // 取消监听eventBus中的why事件
    };
  });
​
  return (
    <div>
      <button onClick={(e) => setCount(count + 1)}>+1({count})</button>
    </div>
  );
});
​
export default App;

Effect性能优化

默认情况下,useEffect的回调函数会在每次渲染时都重新执行,但是这会导致两个问题:

  • 某些代码我们只是希望执行一次即可,类似于componentDidMount和componentWillUnmount中完成的事情;(比如网络请求、订阅和取消订阅);
  • 另外,多次执行也会导致一定的性能问题;

我们如何决定useEffect在什么时候应该执行和什么时候不应该执行呢?

  • useEffect实际上有两个参数:
  • 参数一:执行的回调函数;
  • 参数二:该useEffect在哪些state发生变化时,才重新执行;(受谁的影响)

但是,如果一个函数我们不希望依赖任何的内容时,也可以传入一个空的数组 []:

  • 那么这里的两个回调函数分别对应的就是componentDidMount和componentWillUnmount生命周期函数了;
import React, { memo, useEffect, useState } from "react";
​
const App = memo(() => {
  const [count, setCount] = useState(100);
  const [message, setMessage] = useState("Hello World");
  // 负责告知react,在执行玩当前组件渲染之后要执行的副作用代码
  useEffect(() => {
    // 1.修改document的title
    console.log("修改title");
    // 当count发生变化时才会重新执行
  }, [count]);
​
  // 一个函数式组件中,可以存在多个useEffect
  useEffect(() => {
    // 2.对redux中数据变化监听
    console.log("对redux进行监听");
    return () => {
      // 取消redux中数据的监听
    };
  }, []);
​
  useEffect(() => {
    // 3.监听eventBus中的why事件
    console.log("对eventBus中的why事件监听");
    return () => {
      // 取消监听eventBus中的why事件
    };
  }, []);
​
  useEffect(() => {
    console.log("从服务器获取数据");
  }, []);
​
  return (
    <div>
      <button onClick={(e) => setCount(count + 1)}>+1({count})</button>
      <button onClick={(e) => setMessage("你好啊")}>{message}</button>
    </div>
  );
});
​
export default App;

useContext

在之前的开发中,我们要在组件中使用共享的Context有两种方式:

  • 类组件可以通过 类名.contextType = MyContext方式,在类中获取context;
  • 多个Context或者在函数式组件中通过 MyContext.Consumer 方式共享context;

但是多个Context共享时的方式会存在大量的嵌套:

  • Context Hook允许我们通过Hook来直接获取某个Context的值;
import { UserContext, ThemeContext } from './05-useContext的使用/content';
​
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <UserContext.Provider value={{ name: "why", level: 99 }}>
    <ThemeContext.Provider value={{ color: "red", size: 30 }}>
      <App />
    </ThemeContext.Provider>
  </UserContext.Provider>
);
import { createContext } from "react";
​
const UserContext = createContext()
const ThemeContext = createContext()
​
export {
  UserContext, ThemeContext
}
import React, { memo, useContext } from "react";
import { UserContext, ThemeContext } from "./content";
​
const App = memo(() => {
  // 使用context
  const user = useContext(UserContext);
  const theme = useContext(ThemeContext);
​
  return (
    <div>
      <h2>
        User:{user.name}-{user.level}
      </h2>
      <h2 style={{ color: theme.color, fontSize: theme.size }}>Theme</h2>
    </div>
  );
});
​
export default App;

注意事项:

  • 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重新渲染,并使用最新传递给 MyContext provider 的 context value 值。

useReducer

很多人看到useReducer的第一反应应该是redux的某个替代品,其实并不是。

useReducer仅仅是useState的一种替代方案:

  • 在某些场景下,如果state的处理逻辑比较复杂,我们可以通过useReducer来对其进行拆分;
  • 或者这次修改的state需要依赖之前的state时,也可以使用;
import React, { memo, useReducer, useState } from "react";
​
function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { ...state, counter: state.counter + 1 };
    case "decrement":
      return { ...state, counter: state.counter - 1 };
    case "add_number":
      return { ...state, counter: state.counter + action.num };
    case "sub_number":
      return { ...state, counter: state.counter - action.num };
    default:
      return state;
  }
}
​
const App = memo(() => {
  // const [count, setCount] = useState(0);
​
  const [state, dispatch] = useReducer(reducer, { counter: 0 });
  return (
    <div>
​
      <h2>当前计数:{state.count}</h2>
      <button onClick={(e) => dispatch({ type: "increment" })}>+1</button>
      <button onClick={(e) => dispatch({ type: "decrement" })}>-1</button>
      <button onClick={(e) => dispatch({ type: "add_number", num: 5 })}>
        +5
      </button>
      <button onClick={(e) => dispatch({ type: "sub_number", num: 5 })}>
        -5
      </button>
      <button onClick={(e) => dispatch({ type: "add_number", num: 100 })}>
        +100
      </button>
    </div>
  );
});
​
export default App;

数据是不会共享的,它们只是使用了相同的counterReducer的函数而已。

所以,useReducer只是useState的一种替代品,并不能替代Redux。

useCallback

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

如何进行性能的优化呢?

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

通常使用useCallback的目的是不希望子组件进行多次渲染,并不是为了函数进行缓存;

import React, { memo, useCallback, useRef, useState } from "react";
​
// useCallback性能优化的点:
// 1.当需要一个函数传递给子组件时,最好使用useCallback进行优化,将优化之后的函数,传递给子组件(只有传递的props发生改变时才会重新渲染组件)// 当props发生改变时,组件就会被重新渲染
const LYCIncrement = memo((props) => {
  const { increment } = props;
  console.log("组件被重新渲染");
  return (
    <div>
      <button onClick={increment}>increment+1</button>
    </div>
  );
});
​
const App = memo(() => {
  const [count, setCount] = useState(0);
  const [message, setMassage] = useState("hello");
​
  // 普通的函数
  // const increment = () => {
  //   setCount(count + 1);
  // };
​
  // 闭包陷阱: useCallback
  // const increment = useCallback(
  //   function () {
  //     setCount(count + 1);
  //   },
  //   [count]
  // );
​
  // 进一步优化:当count发生改变时,也使用同一个函数
  // 做法一:将count依赖移除掉,缺点:闭包陷阱
  // 做法二:useRef,在组件渲染时,返回同一个值
  const countRef = useRef();
  countRef.current = count;
  const increment = useCallback(function () {
    setCount(countRef.current + 1);
  }, []);
​
  return (
    <div> 
      <h2>计数:{count}</h2>
      <button onClick={increment}>+1</button>
      <LYCIncrement increment={increment}></LYCIncrement>
      <h2>message:{message}</h2>
      <button onClick={(e) => setMassage(Math.random())}>修改message</button>
    </div>
  );
});
​
export default App;

useMemo

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

◼ 如何进行性能的优化呢?

  • useMemo返回的也是一个 memoized(记忆的) 值;
  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的;
import React, { memo, useMemo, useState } from "react";
​
const HelloWorld = memo(function (props) {
  console.log("Hello World组件被重新渲染了哦");
  return <h2>Hello World</h2>;
});
​
function calcNumTotal(num) {
  console.log("重新计数了哦");
  let total = 0;
  for (let i = 1; i <= num; i++) {
    total += i;
  }
  return total;
}
​
const App = memo(() => {
  const [count, setCount] = useState(0);
​
  // const result = calcNumTotal(50);
​
  // 1.不依赖任何的值,进行计算 只会计算一次
  // let result = useMemo(() => {
  //   return calcNumTotal(50);
  // }, []);
​
  // useMemo拿到的是他的返回值
  // 2.依赖count
  let result = useMemo(() => {
    return calcNumTotal(count * 2);
  }, [count]);
​
  // 3.useMemo实现useCallback功能
  // function fn() {}
  // const increment = useCallback(fn, []);
  // const increment2 = useMemo(() => fn, []);
​
  // 4.使用useMemo对子组件渲染进行优化
  // const info = { name: "lyc", age: 18 }; 会使子组件重新渲染
  const info = useMemo(() => ({ name: "lyc", age: 18 }), []);
​
  return (
    <div>
      <h2>计算结果: {result}</h2>
      <h2>计数器:{count}</h2>
      <button onClick={(e) => setCount(count + 1)}>+1</button>
      <HelloWorld result={result} info={info}></HelloWorld>
    </div>
  );
});
​
export default App;

useRef

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

最常用的ref是两种用法:

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

绑定DOM

import React, { memo, useRef } from "react";
​
const App = memo(() => {
  const titleRef = useRef();
  const inputRef = useRef();
​
  function showTitleDom() {
    console.log(titleRef.current);
    inputRef.current.focus();
  }
  return (
    <div>
      <h2 ref={titleRef}>Hello World</h2>
      <input type="text" ref={inputRef} />
      <button onClick={showTitleDom}>查看title的dom</button>
    </div>
  );
});
​
export default App;

保存数据

import React, { memo, useCallback, useRef, useState } from "react";
​
let obj = null;
​
const App = memo(() => {
  const [count, setCount] = useState(0);
  const nameRef = useRef();
  console.log(obj === nameRef);
  obj = nameRef;
​
  // 通过useRef解决闭包陷阱
  const countRef = useRef();
  countRef.current = count;
​
  const increment = useCallback(() => {
    setCount(countRef.current + 1);
  }, []);
​
  return (
    <div>
      <h2>Hello World:{count}</h2>
      <button onClick={(e) => setCount(count + 1)}>+1</button>
      <button onClick={increment}>+1</button>
    </div>
  );
});
​
export default App;

其他Hooks的使用

useImperativeHandle

我们先来回顾一下ref和forwardRef结合使用:

  • 通过forwardRef可以将ref转发到子组件;
  • 子组件拿到父组件中创建的ref,绑定到自己的某一个元素中;

forwardRef的做法本身没有什么问题,但是我们是将子组件的DOM直接暴露给了父组件:

  • 直接暴露给父组件带来的问题是某些情况的不可控;
  • 父组件可以拿到DOM后进行任意的操作;
  • 但是,事实上在上面的案例中,我们只是希望父组件可以操作的focus,其他并不希望它随意操作;

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

  • 通过useImperativeHandle的Hook,将传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起;
  • 所以在父组件中,使用 inputRef.current时,实际上使用的是返回的对象;
  • 比如我调用了 focus函数,甚至可以调用 printHello函数;
import React, { memo, useRef, forwardRef, useImperativeHandle } from "react";
​
const HelloWorld = memo(
  forwardRef((props, ref) => {
    // 可以在子组件内部定义ref
    const inputRef = useRef();
​
    // 子组件对父组件传入的ref进行处理
    useImperativeHandle(ref, () => {
      return {
        focus() {
          console.log("focus");
          inputRef.current.focus();
        },
        setValue(value) {
          inputRef.current.value = value;
        },
      };
    });
​
    // 如果ref直接绑定组件,就会可能有操作危险
    return <input type="text" ref={inputRef} />;
  })
);
const App = memo(() => {
  const titleRef = useRef();
  const inputRef = useRef();
​
  function handleDOM() {
    // console.log(inputRef.current);
    inputRef.current.focus();
    // inputRef.current.value = "";
    inputRef.current.setValue("哈哈哈");
  }
  return (
    <div>
      <h2 ref={titleRef}>哈哈哈</h2>
      <HelloWorld ref={inputRef} />
      <button onClick={handleDOM}>DOM操作</button>
    </div>
  );
});
​
export default App;

useLayoutEffect

useLayoutEffect看起来和useEffect非常的相似,事实上他们也只有一点区别而已:

  • useEffect会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新;
  • useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新;

如果我们希望在某些操作发生之后再更新DOM,那么应该将这个操作放到useLayoutEffect。

image-20221116170348305

官方更推荐使用useEffect而不是useLayoutEffect。

import React, { memo, useEffect, useLayoutEffect, useState } from "react";
​
const App = memo(() => {
  const [count, setCount] = useState(0);
​
  useEffect(() => {
    console.log("useEffect");
  });
​
  useLayoutEffect(() => {
    console.log("useLayoutEffect");
  });
​
  console.log("App render");
​
  return (
    <div>
      <h2>count:{count}</h2>
      <button onClick={(e) => setCount(count + 1)}> +1 </button>
    </div>
  );
});
​
export default App;

useId

官方的解释:useId 是一个用于生成横跨服务端和客户端的稳定的唯一 ID 的同时避免 hydration 不匹配的 hook。

这里有一个词叫hydration,要想理解这个词,我们需要理解一些服务器端渲染(SSR)的概念。

什么是SSR?
  • SSR(Server Side Rendering,服务端渲染),指的是页面在服务器端已经生成了完成的HTML页面结构,不需要浏览器解析;
  • 对应的是CSR(Client Side Rendering,客户端渲染),我们开发的SPA页面通常依赖的就是客户端渲染;image-20221117165149043

早期的服务端渲染包括PHP、JSP、ASP等方式,但是在目前前后端分离的开发模式下,前端开发人员不太可能再去学习PHP、JSP等技术来开发网页;

不过我们可以借助于Node来帮助我们执行JavaScript代码,提前完成页面的渲染;

SPA的缺陷和服务器渲染SSR的优势

image-20221116200730442

SSR同构应用

什么是同构?

  • 一套代码既可以在服务端运行又可以在客户端运行,这就是同构应用。

同构是一种SSR的形态,是现代SSR的一种表现形式。

  • 当用户发出请求时,先在服务器通过SSR渲染出首页的内容。
  • 但是对应的代码同样可以在客户端被执行。
  • 执行的目的包括事件绑定等以及其他页面切换时也可以在客户端被渲染;

image-20221117165657631

Hydration

◼ 什么是Hydration?这里我引入vite-plugin-ssr插件的官方解释。

image-20221117165750681

在进行 SSR 时,我们的页面会呈现为 HTML

  • 但仅 HTML 不足以使页面具有交互性。例如,浏览器端 JavaScript 为零的页面不能是交互式的(没有 JavaScript 事件处理程序来响应用户操作,例如单击按钮)。
  • 为了使我们的页面具有交互性,除了在 Node.js 中将页面呈现为 HTML 之外,我们的 UI 框架(Vue/React/...)还在浏览器中加载和呈现页面。(它创建页面的内部表示,然后将内部表示映射到我们在 Node.js 中呈现的 HTML 的 DOM 元素。)

这个过程称为hydration

useId的作用

我们再来看一遍:useId 是一个用于生成横跨服务端和客户端的稳定的唯一 ID 的同时避免 hydration 不匹配的 hook。

所以我们可以得出如下结论:

  • useId是用于react的同构应用开发的,前端的SPA页面并不需要使用它;
  • useId可以保证应用程序在客户端和服务器端生成唯一的ID,这样可以有效的避免通过一些手段生成的id不一致,造成hydration mismatch;
import React, { memo, useId, useState } from "react";
​
const App = memo(() => {
  const [count, setCount] = useState(0);
  const id = useId();
  console.log(id);
  return (
    <div>
      <button onClick={(e) => setCount(count + 1)}>count+1:{count}</button>
​
      <label htmlFor={id}>
        用户名:
        <input type="text" id={id} />
      </label>
    </div>
  );
});
​
export default App;

useTransition

官方解释: 返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数。

  • 事实上官方的说法,还是让人云里雾里,不知所云。

useTransition到底是干嘛的呢?它其实在告诉react对于某部分任务的更新优先级较低,可以稍后进行更新。

import React, { memo, useState, useTransition } from "react";
import namesArray from "./namesArray";
​
const App = memo(() => {
  const [showNames, setShowNames] = useState(namesArray);
​
  //useTransition会延迟更新,让html页面先渲染再进行高耗时的js操作后再次渲染
  const [pending, startTransition] = useTransition();
​
  function valueChangeHandle(e) {
    startTransition(() => {
      const keyword = e.target.value;
      const filterShowNames = namesArray.filter((item) =>
        item.includes(keyword)
      );
      setShowNames(filterShowNames);
    });
  }
​
  return (
    <div>
      <input type="text" onInput={(e) => valueChangeHandle(e)} />
      <h2>用户名列表:{pending && <span>data loading</span>}</h2>
      <ul>
        {showNames.map((item, index) => {
          return <li key={index}>{item}</li>;
        })}
      </ul>
    </div>
  );
});
​
export default App;

useDeferredValue

官方解释:useDeferredValue 接受一个值,并返回该值的新副本,该副本将推迟到更紧急地更新之后。

在明白了useTransition之后,我们就会发现useDeferredValue的作用是一样的效果,可以让我们的更新延迟。

import React, { memo, useDeferredValue, useState, useTransition } from "react";
import namesArray from "./namesArray";
​
const App = memo(() => {
  const [showNames, setShowNames] = useState(namesArray);
  // useDeferredValue 会生成一个延迟渲染的数据
  const deferedShowNames = useDeferredValue(showNames);
​
  function valueChangeHandle(e) {
    const keyword = e.target.value;
    const filterShowNames = namesArray.filter((item) => item.includes(keyword));
    setShowNames(filterShowNames);
  }
​
  return (
    <div>
      <input type="text" onInput={(e) => valueChangeHandle(e)} />
      <h2>用户名列表:</h2>
      <ul>
        {deferedShowNames.map((item, index) => {
          return <li key={index}>{item}</li>;
        })}
      </ul>
    </div>
  );
});
​
export default App;

自定义Hooks使用

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

案例一:所有的组件在创建和销毁时都进行打印

import React, { memo, useEffect, useState } from "react";
​
function useLogLife(cName) {
  useEffect(() => {
    console.log(cName + "组件被创建");
    return () => {
      console.log(cName + "组件被销毁");
    };
  }, [cName]);
}
​
const Home = memo(() => {
  useLogLife("home");
  return <h1>Home Page</h1>;
});
​
const About = memo(() => {
  useLogLife("about");
  return <h1>About Page</h1>;
});
​
const App = memo(() => {
  const [isShow, setIsShow] = useState(true);
  useLogLife("root");
  return (
    <div>
      <h1>App Root Component</h1>
      <button onClick={(e) => setIsShow(!isShow)}>切换</button>
      {isShow && <Home />}
      {isShow && <About />}
    </div>
  );
});
​
export default App;

案例二:Context的共享

import React, { memo } from "react";
import { useUserToken } from "./hooks";
​
const Home = memo(() => {
  const [user, token] = useUserToken();
  return (
    <h1>
      Home Page:{user.name}-{token}
    </h1>
  );
});
​
const About = memo(() => {
  const [user, token] = useUserToken();
  return (
    <h1>
      About Page:{user.name}-{token}
    </h1>
  );
});
​
const App = memo(() => {
  return (
    <div>
      <h1>App Root Component</h1>
      <Home />
      <About />
    </div>
  );
});
​
export default App;

./hooks/useUserToken.js

import { useContext } from "react";
import { UserContext, TokenContext } from "../context";
​
function useUserToken() {
  const user = useContext(UserContext)
  const token = useContext(TokenContext)
​
  return [user, token]
}
​
export default useUserToken

./context/index.js

import { createContext } from "react";
​
const UserContext = createContext()
const TokenContext = createContext()
​
export {
  UserContext, TokenContext
}

index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import { UserContext, TokenContext } from './12-自定义Hooks/context';
​
import App from './12-自定义Hooks/App';
​
​
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <UserContext.Provider value={{ name: "why", level: 99 }}>
    <TokenContext.Provider value={'lyc'}>
      <App />
    </TokenContext.Provider>
  </UserContext.Provider>
);

案例三:获取滚动位置

import React, { memo } from "react";
import "../12-自定义Hooks/style.css";
import { useScrollPosition } from "./hooks";
​
const Home = memo(() => {
  const [scrollX, scrollY] = useScrollPosition();
​
  return (
    <h1>
      Home Page:{scrollX}-{scrollY}
    </h1>
  );
});
​
const About = memo(() => {
  const [scrollX, scrollY] = useScrollPosition();
  return (
    <h1>
      About Page:{scrollX}-{scrollY}
    </h1>
  );
});
​
const App = memo(() => {
  return (
    <div className="app">
      <h1>App Root Component</h1>
      <Home />
      <About />
    </div>
  );
});
​
export default App;

./hooks/useScrollPosition

import { useEffect, useState } from "react";
​
function useScrollPosition() {
  const [scrollX, setScrollX] = useState(0)
  const [scrollY, setScrollY] = useState(0)
​
  useEffect(() => {
    function handleScroll() {
      setScrollX(window.scrollX)
      setScrollY(window.scrollY)
    }
​
    window.addEventListener("scroll", handleScroll);
​
    return () => {
      window.removeEventListener("scroll", handleScroll);
    };
  }, []);
​
  return [scrollX, scrollY]
}
​
export default useScrollPosition

案例四:localStorage数据存储

import React, { memo, useEffect, useState } from "react";
import { useLocalStorage } from "./hooks";
​
const App = memo(() => {
  // 通过key,直接从loaclStorage中获取一个数据
  const [token, setToken] = useLocalStorage("token");
  function setTokenHandle() {
    setToken("james");
  }
​
  const [avatarUrl, setAvatarUrl] = useLocalStorage("avatarUrl");
​
  function setAvatarUrlHandle() {
    setAvatarUrl("http://www.roydust.com/abc.png");
  }
​
  return (
    <div className="app">
      <h1>App Root Component: {token}</h1>
      <button onClick={setTokenHandle}>设置token</button>
      <h1>avatarURL:{avatarUrl}</h1>
      <button onClick={setAvatarUrlHandle}>设置新头像地址</button>{" "}
    </div>
  );
});
​
export default App;
​

./hooks/useLocalStorage.js

import { useEffect, useState } from "react"function useLocalStorage(key) {
  const [data, setData] = useState(() => {
    // 1.从loacalStorage中获取数据,并且数据创建的组件的state
    const item = JSON.parse(localStorage.getItem(key))
    if (!item) return ""
    return JSON.parse(item)
  })
​
  // 2.监听data改变,一旦发生改变就会存储data的最新值
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(data))
  }, [data])
​
  // 3.将data/setData的操作返回组件,让组件可以使用和修改
  return [data, setData]
}
​
export default useLocalStorage

redux hooks

在之前的redux开发中,为了让组件和redux结合起来,我们使用了react-redux中的connect:

  • 但是这种方式必须使用高阶函数结合返回的高阶组件;
  • 并且必须编写:mapStateToProps和 mapDispatchToProps映射的函数;

在Redux7.1开始,提供了Hook的方式,我们再也不需要编写connect以及对应的映射函数了

useSelector的作用是将state映射到组件中:

  • 参数一:将state映射到需要的数据中;
  • 参数二:可以进行比较来决定是否组件重新渲染;

useSelector默认会比较我们返回的两个对象是否相等;

  • 如何比较呢? const refEquality = (a, b) => a === b;
  • 第二参数可以传入shallowEqual会比较映射前后的数据变化 如果没有变化就不会重新渲染
  • 也就是我们必须返回两个完全相等的对象才可以不引起重新渲染;

useSelector监听的是全部的state,所以在父组件的count发生变化时,子组件也会重新渲染

image-20221116193431350

useDispatch非常简单,就是直接获取dispatch函数,之后在组件中直接使用即可;

我们还可以通过useStore来获取当前的store对象;

import React, { memo } from "react";
import { useSelector, useDispatch, shallowEqual } from "react-redux";
import {
  addNumberAction,
  changeMessageAction,
  subNumberAction,
} from "./store/modules/counter";
​
// memo高阶组件包裹起来的组件有对应的特点: 只有props发生改变时, 才会重新渲染
const Home = memo((props) => {
  const { message } = useSelector(
    (state) => ({
      message: state.counter.message,
    }),
    // shallowEqual 会比较映射前后的数据变化 如果没有变化就不会重新渲染
    shallowEqual
  );
​
  const dispatch = useDispatch();
  function changeMessageHandle() {
    dispatch(changeMessageAction("你好啊, 师姐!"));
  }
​
  console.log("Home render");
​
  return (
    <div>
      <h2>Home: {message}</h2>
      <button onClick={(e) => changeMessageHandle()}>修改message</button>
    </div>
  );
});
​
const App = memo((props) => {
  // 1.使用useSelector将redux中store的数据映射到组件内
  const { count } = useSelector(
    (state) => ({
      count: state.counter.count,
    }),
    shallowEqual
  );
​
  // 2.使用dispatch直接派发action
  const dispatch = useDispatch();
  function addNumberHandle(num, isAdd = true) {
    if (isAdd) {
      dispatch(addNumberAction(num));
    } else {
      dispatch(subNumberAction(num));
    }
  }
​
  console.log("App render");
​
  return (
    <div>
      <h2>当前计数: {count}</h2>
      <button onClick={(e) => addNumberHandle(1)}>+1</button>
      <button onClick={(e) => addNumberHandle(6)}>+6</button>
      <button onClick={(e) => addNumberHandle(6, false)}>-6</button>
​
      <Home />
    </div>
  );
});
​
export default App;

ReactHooks的闭包陷阱

import { useEffect, useState } from 'react';
​
function Dong() {
​
    const [count,setCount] = useState(0);
​
    useEffect(() => {
        setInterval(() => {
            setCount(count + 1);
        }, 500);
    }, []);
​
    useEffect(() => {
        setInterval(() => {
            console.log(count);
        }, 500);
    }, []);
​
    return <div>guang</div>;
}
​
export default Dong;

用 useState 创建了个 count 状态,在一个 useEffect 里定时修改它,另一个 useEffect 里定时打印最新的 count 值。

我们跑一下:

img

打印的并不是我们预期的 0、1、2、3,而是 0、0、0、0,这是为什么呢?

这就是所谓的闭包陷阱!

如何解决呢?

import { useEffect, useState } from 'react';
​
function Dong() {
​
    const [count,setCount] = useState(0);
​
    useEffect(() => {
        setInterval(() => {
            setCount(count + 1);
        }, 500);
    }, [count]);
    //监听了count
    useEffect(() => {
        setInterval(() => {
            console.log(count);
        }, 500);
    }, [count]);
​
    return <div>guang</div>;
}
​
export default Dong;

这样每次 count 变了就会执行引用了最新 count 的函数了:

img

现在确实不是全 0 了,但是这乱七八遭的打印是怎么回事?

那是因为现在确实是执行传入的 fn 来设置新定时器了,但是之前的那个没有清除呀,需要加入一段清除逻辑:

import { useEffect, useState } from 'react';
​
function Dong() {
​
    const [count,setCount] = useState(0);
​
    useEffect(() => {
        const timer = setInterval(() => {
            setCount(count + 1);
        }, 500);
        return () => clearInterval(timer);
    }, [count]);
​
    useEffect(() => {
        const timer = setInterval(() => {
            console.log(count);
        }, 500);
        return () => clearInterval(timer);
    }, [count]);
​
    return <div>guang</div>;
}
​
export default Dong;

加上了 clearInterval,每次执行新的函数之前会把上次设置的定时器清掉。

再试一下:

img

现在就是符合我们预期的了,打印 0、1、2、3、4。

很多同学学了 useEffect 却不知道要返回一个清理函数,现在知道为啥了吧。就是为了再次执行的时候清掉上次设置的定时器、事件监听器等的。

这样我们就完美解决了 hook 闭包陷阱的问题。

我们了解 hooks 的实现原理:

从根上理解 React Hooks 的闭包陷阱

在 fiber 节点的 memorizedState 属性存放一个链表,链表节点和 hook 一一对应,每个 hook 都在各自对应的节点上存取数据。

useEffect、useMomo、useCallback 等都有 deps 的参数,实现的时候会对比新旧两次的 deps,如果变了才会重新执行传入的函数。所以 undefined、null 每次都会执行,[] 只会执行一次,[state] 在 state 变了才会再次执行。

闭包陷阱产生的原因就是 useEffect 等 hook 里用到了某个 state,但是没有加到 deps 数组里,这样导致 state 变了却没有执行新传入的函数,依然引用的之前的 state。

闭包陷阱的解决也很简单,正确设置 deps 数组就可以了,这样每次用到的 state 变了就会执行新函数,引用新的 state。不过还要注意要清理下上次的定时器、事件监听器等。

要理清 hooks 闭包陷阱的原因是要理解 hook 的原理的,什么时候会执行新传入的函数,什么时候不会。

hooks 的原理确实也不难,就是在 memorizedState 链表上的各节点存取数据,完成各自的逻辑的,唯一需要注意的是 deps 数组引发的这个闭包陷阱问题。