2022 react hooks 看这一篇就够了

1,554 阅读26分钟

author 张向强

马上2022了,才开始说hooks,是不是有点晚?

不晚,不晚,总有新的小友,对它不那么熟悉,那这一篇从零开始,简洁有力的文章一定会很有帮助。

理论指导实践,有了理论依据我们才能跑的飞快,为了不误人子弟,我又重读了一遍官方文档(冗长而琐碎需要耐心的文档),所以文内最重要的理论依据会来源于官方文档摘抄(我会标识出来),请放心食用。

我希望你最好有一点react基础,一点点就够了,差不多就是能用react画个页面,然后知道一些生命周期的程度。

好了,闲话少说,我们开始。

Hooks 出现的背景

官方是这么介绍hooks的,Hook 是 React 16.8 的新增特性,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

意思就是,hooks让你可以写函数组件,让函数组件的能力像类组件一样强大。

它出现的原因,简单来说有这么三个原因(官方写的有点绕,我用人话说一遍):

  1. 是在类组件之间复用状态逻辑很难(类组件之前一般使用render props和高阶组件的方式来复用逻辑)。Hook 使你在无需修改组件结构的情况下复用状态逻辑

  2. 复杂的类组件会变的很难理解。比如,很多时候相互关联的逻辑要同时写在componentDidMount,componentDidUpdate,componentWillUnmount几个生命周期中,逻辑一旦复杂起来维护就很麻烦。再比如,我常常会在一个生命周期里写很多不相互关联的逻辑。 我们希望的是,相关的逻辑写一起,不相关的就分开写。但是之前我们只能写在react暴露的生命周期里,业务逻辑一旦堆起来,就变的又杂糅又分散,这也是无奈之举。为了解决这个问题,Hook 将组件中相互关联的部分拆分成更小的函数,(也就是相关的逻辑可以写在同一个hook里),而并非强制按照生命周期划分。

  3. Class 难以理解。一个是要熟悉js里的this的指向,类里经常要用到,另一个是class 不容易优化。

hooks 的使用原则

只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用;

只能在 React 的函数组件中调用 Hook;

这两条原则出现的原因呢,我们后面说到原理再解释,说完了hook 使用原则,然后我们来具体说说这些神通广大的hook。

useState

useState,是干嘛的呢,存状态,改状态的,useState 解决了函数组件没有 state 的问题,让无状态组件有了自己的状态,我们知道react 用状态来控制着页面视图的更新。

useState()这个函数接受状态的初始值,作为参数;该函数返回一个数组,数组的第一个成员是一个变量,指向状态的当前值,第二个成员是一个函数,用来更状态,约定是set前缀加上状态的变量名。

我们写一个计数器组件,来展示的useState基础用法:

import React, { useState } from "react";
​
export default function  Example()  {
  // 声明一个叫 "count" 的 state 变量,然后把它的初始值设为 0, 我们可以通过调用 setCount 来更新当前的 count
  const [count, setCount] = useState(0);
​
  function handleClick()  {
    return setCount(count+1);
  }
​
  return  <button  onClick={handleClick}>{count}</button>;
}

我们看到基础用法很简单,一看就会,那我们进阶一下。

批量更新

在此之前,我们先回忆一下react类组件对于状态的处理机制有两个关键点,批量更新,异步(模拟的异步)处理;

批量更新: 调用this.setState后状态并没有立即更新,而是先缓存起来了,等事件函数处理完后,再进行批量更新,一次更新并重新渲染。因此,我们更新状态后,如果立即去拿最新的状态是拿不到的。但并不是所有情况下,都会批量更新,我们可以这么理解,只要在react控制的区域里就会批量更新,比如: 事件处理函数,生命周期函数,只要不归react管就是非批量,比如定时器,promise等,可以立即拿到最新状态

这种优化性能的机制在函数组件里依旧保留着,那就有了两个问题,先看思考第一个,如果我们的状态依赖于前一个状态(就是我们要拿到最新状态),该怎么处理呢?先对批量更新做个实验,熟悉一下:

...
const [count, setCount] = useState(0);
​
function handleClick()  {
    setCount(count+1);
    console.log(count); // 0
    setCount('聪明蛋');
    console.log(count); // 0
    setTimeout(()=>{
      console.log(count); // 0
      setCount(count+1); 
      console.log(count); // 0
    }, 1000)
    console.log(count, 1); // 0,1
  }
  
  return  <button  onClick={handleClick}>{count}</button>
 ...
​

看看注释的打印数据,是不是和预期的有些差异,打印结果是0,0, 0 1,0, 0;前两个打印和预期是一致的,按钮展示结果从0变成了'聪明蛋'(1s中后变成1),这就是批量更新嘛,对于同一个状态值后面的覆盖前面的么,很完美? 但是从定时器里面,就开始风格诡异了,为什么第一个打印的是0啊,不应该是'聪明蛋'么?为什么后面都是0啊,这世界怎么了?

好吧,没想到这么快就遇到hooks的一个陷阱,这个就叫闭包陷阱。不过稍微等一下,一个一个来,等我们先把上面的拿到最新状态的问题搞定,再来填这个坑。

大脑微微转动一下,什么时候我们需要拿最新的状态呢,有这个场景么?

假如我设置了状态后,需要用这个设置状态后面的状态,做些什么事,那么能设置状态就说明我们能拿到设置状态的变量,我们直接用这个变量去做不就好了,这是什么脑残问题?刷刷如下:

function handleClick()  {
    ...
    const a = xxx;
    setCount(a);
    // 想用最新的count 做些啥,直接用a 不就好了
    
  }

嗯,上面这个想法其实是走进误区了(但不一定没用哦,没用的我不会写,这里主要是提醒一部分小友,很多场景不需要用到最新状态去做,不要执着于这个),首先你要用的那个变量不一定在一个函数内,不一定好拿,真实场景可能是这样的,比如一个相对复杂的输入组件,我要把最新的输入值和之前存的状态值合并起来得到新的状态值,比如我输入了1,我把原来的状态的某个字段改为1;

const [value, setValue] = useState({a: 1, b:2, c:3});
​
原来的状态: {
  a: 1,
  b: 2,
  c: 3
}
​
我要的状态: {
a: 1,
b: 2,
c: 1,   // 我就是想改下c
}

你如果直接setValue({c: 1}),那之前的值就丢了。我们可以这样改:

setValue((state)=>({...state, c: 1}));

官方说法: 如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值;

这样写,很方便,不是么?可能有人有不同意见,觉得这种情况,我本来就可以拿到最新的值,不需要用函数拿,直接用不就好了,我完全可以先改了原来的状态 value.c = 1, 然后在setValue,不就行了,于是有一下代码:

...
value.c = 1;
setValue(value);

​ 很好,天才想法!结果发现不生效,为啥呢,后面咱讲到原理再聊。先说下上面遇到的闭包陷阱。

闭包陷阱

先copy一下咱上面实验批量更新的代码和思路,然后再继续:

import React, { useState } from "react";
​
export default function  Example()  {
  // 声明一个叫 "count" 的 state 变量,然后把它的初始值设为 0, 我们可以通过调用 setCount 来更新当前的 count
  const [count, setCount] = useState(0);
​
  function handleClick()  {
    setCount(count+1);
    console.log(count); // 0
    setCount('聪明蛋');
    console.log(count); // 0
    setTimeout(()=>{
      console.log(count); // 0
      setCount(count+1); 
      console.log(count); // 0
      setCount((state=>{
        console.log(state); // 1
        setCount(state);
      }))
    }, 1000)
  }
​
  return  <button  onClick={handleClick}>{count?.a}{count?.b}{count?.c}</button>;
}

我们点击一次,handleClick里,是不是和预期的有些差异,头两个打印和预期是一致的,按钮展示结果从0变成了'聪明蛋'(1s中后变成1),这就是批量更新嘛,对于同一个状态值后面的覆盖前面的么,很完美? 但是从定时器里面,就开始风格诡异了,为什么第一个打印的是0啊,不应该是'聪明蛋'么?为什么后面都是0啊,这世界怎么了?

好了,上面是复制的内容,咱们继续,先说说闭包,通俗一点说就是:一个函数返回一个不销毁的执行上下文(内部有变量被外界引用)。

那么我们在分析一下上面那段代码,当我们点击时,handleClick更新了状态,执行到setTimeout,发现是异步,于是把异步代码放进异步队列里存起来,注意,这时handleClick 里有定时器,所以这个handleClick执行到末尾时,这个作用域并不会被销毁,这就使得它外层的Example函数组件执行上下文不会被销毁,但是状态更新了,react要去更新组件Example,Example 函数再次执行,视图渲染,按钮展示聪明蛋。然后时间到了,定时器开始执行,顺着作用域链去拿count,但是它只能拿到闭包里的count 变量(0),因为作用域是在函数定义是确定下来的,在它定义时,它的上级作用域里count就是0;然后它更新了一下,把count 变成1;

好啦,上面就是闭包陷进啦,简单说就是,咱在某个没有销毁的执行上下文里用了状态,造成里面的状态只能取到闭包里的值,而不是更新后的值。

那,知道了这个陷阱,那我们要怎么破解呢,很显然上面代码其实已经给了答案(没注意的可以回头看看注释的打印),给setSate 传函数就可以解。

那我们在想一下,既然是因为闭包影响,那我把这个受影响的值存到闭包的外面(上层,比如全局),不就可以了么,所以还有第二种解法,使用useRef:

import React, { useState,useRef } from "react";
​
export default function  Example()  {
  // 声明一个叫 "count" 的 state 变量,然后把它的初始值设为 0, 我们可以通过调用 setCount 来更新当前的 count
  const [count, setCount] = useState(0);
  const R = useRef();
  R.current = count;
​
  function handleClick()  {
    setCount(count+1);
    console.log(count); // 0
    setCount('聪明蛋');
    console.log(count); // 0
    setTimeout(()=>{
      console.log(R.current); // 聪明蛋
      console.log(count); // 0
      setCount(count+1); 
      console.log(R.current, 'R'); // 1, 注意这里是里立即拿到,最新的状态值哦,说明批量更新机制是完全生效的
      console.log(count); // 0
      setCount((state=>{
        console.log(state); // 1
        console.log(R.current); // 1
        setCount(state);
      }))
    }, 1000)
    console.log(count, 1); // 0,1
  }
​
  return  <button  onClick={handleClick}>{count}</button>;
}
​

为什么这种写法可以呢,我们稍后讲到useRef 再详细解释。我们先把useState 再进阶一下。

总结一句哈,遇到怪事就想想定时器,想想闭包,在hook中用定时器,得注意一点哦。

useState原理

写了半天,useState 还没写完,我真是醉了,感叹一句写文不易,且读且珍惜;也不着急,无非是多花我几个小时,我花费的只是时间,你们得到的可是财富啊。

我们已经知道了useSate的用法,那么闭上眼睛,沉吟30秒,让我们想一想,如果让你写个useState 你怎么写呢?

根据常规用法,那我们能很快想到,首先有个函数,接收一个初始值,返回一个数组,数组第一个值时一个状态,第二个值是个函数改变这个状态,我们可以很快写出如下代码:

const useState = (init) => {
​
  let state = init;
​
  const setState = (newState) => { // 一个改状态的方法
​
    state = newState;
​
  }
​
  return [ // 先写返回
​
    state,
​
    setState,
​
  ]
​
}

很完美,如果别人去用我们这个这个useState,会怎样?

至少有这么两个问题:

问题1: 我们的状态会被重复初始化

第一次渲染是ok的,但是第二次更新会出问题,因为我们把state这个变量保存在里useState里,函数更新重新执行useState,我们的状态就又变成了初始值,所以我们要把state这个变量提出来,至少不能被组件函数执行影响到的地方。

问题2:一个函数组件里可能会有多个useState,我们如何保证他们执行的顺序,如何分辨他们谁是谁呢?

所以,我们不仅要把state变量提出来,还需要一个地方,把每个useState的返回值存起来,而且要记住他们的顺序。

于是,我们把上面的函数改进一下:

const hookStates = [];
let hookIndex = 0;
​
const useState = (init) => {
  hookIndex++;
  hookStates[hookIndex] = hookStates[hookIndex] || init;
  const setState = (newState) => {
      hookStates[hookIndex] = newState;
      // 然后开始进入更新应用逻辑
      ...
    }
  return [hookStates[hookIndex], setState];
}

这样是不是就解决了上面的问题了,用hookIndex记录hook的顺序,在用个数组把hooks的返回值存起来就好了呗,把有小友可能会怀疑,这个useState它正经吗,能用吗,放心拿去用,没得问题。

不过嘛,毕竟是咱自己模拟的,只能模拟下原理,和源代码还是有那么一点点差异的,源码里呢,hooks状态是存在函数组件对应的fiber节点上,也不是用数组存储,是用链表来存储hook的相关信息,顺序呢,就是每次找当前节点的next就好了。useState的参数可以是一个值或者一个函数,是函数时会把它执行,然后取返回值。不过道理呢就是这个道理,也差不多。react hooks 大部分都类似这种机制。

这个过程中解释了一个问题,就是hooks 规则,hooks 为什么要通常放在顶部,hooks 不能写在 if 条件语句中,因为在更新过程中,如果通过 if 条件语句,增加或者删除 hooks,在复用 hooks 过程中,会产生复用 hooks 状态和当前 hooks 不一致的问题。

useEffect

useEffect() 用来引入具有副作用的操作;

解释一下副作用: 我们想在 React 更新 DOM 之后运行一些额外的代码,网络请求,操作dom,订阅外部数据源,我们基本上都希望在 React 更新 DOM 之后才执行我们的操作;

useEffect()的用法如下:

useEffect(()  =>  {
  // Async Action
  return clearFn();
}, [dependencies])

上面用法中,useEffect() 接受两个参数。第一个参数是一个函数,异步操作代码放里面,如果这个函数返回了一个函数,那么返回的这个函数会被当做清除副作用的函数。第二个参数是一个数组,用于给出Effect的依赖项,只要这个数组发生变化,useEffect() 就会执行。第二个参数可以省略,这样每次组件函数执行时,都会执行useEffect()。

使用useEffect要注意这么几点,官方是这么说的(括号里是我的补充):

传递给 useEffect 的函数在每次渲染中都会有所不同(是一个新创建的函数), 每次我们重新渲染,都会生成新的 effect,替换掉之前的, 这也是我们可以在 effect 中获取最新的状态值的原因;

React 会在执行当前 effect 之前对上一个 effect 进行清除(如果useEffect里的副作用函数返回了清除函数,就会在这时执行), 先清除副作用,后执行, 清除函数会在组件卸载前也会执行;

传给 useEffect 的函数会在浏览器完成布局与绘制,画面渲染之后,在一个延迟事件中被调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因为绝大多数操作不应阻塞浏览器对屏幕的更新。

如果要与类组件对比呢,我们可以这么理解理解: useEffect相当于,componentDidMount,componentDidUpdate和componentWillUnmount的集合,它以一抵三。hooks可以反复多次使用,相互独立。所以我们合理的做法是,给每一个副作用一个单独的useEffect钩子。

用法和注意事项就这么多,那么,我们再写个简单demo吧,我们为上面咱写的计数器增加了一个小功能:将 document 的 title 设置为包含了点击次数的消息(没有写依赖数组,所以每次渲染副作用都会执行):

import React, { useState, useEffect } from 'react';
​
function Example() {
  const [count, setCount] = useState(0);
​
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
​
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        点我
      </button>
    </div>
  );
}

好了,useEffect讲完了,关键点都在上面,就这么多东西,反复阅读就ok了,不过看起来内容有点少(很多文章讲闭包陷阱都用写定时器的栗子来讲,会用到useEffect,不过这个内容我上面讲过,就不重复了,怎么跳出陷阱的原理已经讲了,你学会了么?有兴趣自己实现一个每秒加一的计数器效果,考验一下自己吧),这个hook还是用的挺多,那再补充一点内容吧,说下useEffect的依赖。

对于依赖数组内部元素,使用的是浅比较(===, 引用相等),如果数组内部依赖的引用变了,会当成依赖变了,所以如果在渲染过程中传递函数,对象,不用useCallback , useMemo 包裹的话,会造成依赖每次都变的效果; 所以建议useEffect 内使用函数,函数 写在useEffect 内,或者组件函数外, 实在不行用useCallback 包裹,作为useEffect依赖;

如果依赖是一个空数组,就是让副作用函数只用挂载后,和卸载前执行。

useEffect 原理

说下useEffect原理,和useState 其实是类似的,咱模拟一下吧:

const hookStates = [];// 保存数据状态的数组,每个组件只有一个
let hookIndex = 0; // 索引// useLayoutEffect(这个hook咱们后面说) 和 useEffect 实现一样,只不过useLayoutEffect是同步的(在同步代码的最后执行), useEffect 用宏任务,还多了个销毁步骤
function useEffect(callback, deps){
  if(hookStates[hookStates]){ // 说明不是第一次
    let [oldDestroy,lastDeps] = hookStates[hookIndex];
    const same = deps.every((item, index) => item === lastDeps[index]); // 浅比较
    if(same){
      hookIndex++; // 如果依赖相同就不执行副作用
    }else{
      oldDestroy(); // 执行副作用前,先执行清除函数
      let destroy;
      // 添加一个宏任务,在本次渲染之后执行
      setTimeout(()=>{
        destroy = callback();
        hookStates[hookIndex++] = [destroy,deps]; // 存一下清除函数,和依赖,下次更新用
      }, 0);
    }
  }else{ // 初始化
    let destroy;
    // 添加一个宏任务,在本次渲染之后执行
    setTimeout(()=>{
      destroy = callback();
      hookStates[hookIndex++] = [destroy,deps];
    }, 0);
  }
}
​

那有小友可能就要问了,这个高仿useEffect和源码的差别又在那里呢,特别是在执行的时机那里,是用的定时器么?嗯,原理差不多啦。react源码不是很好看,但是原理说清了,再看起来就会简单很多,小友有时间可以找源码研究一哈。

这个高仿useEffect,除了上面已经说过的存储位置和存储数据结构差异外,还有执行时机的区别,源码里不是直接用setTimeout这个api实现的,在React 17 中,这个特性是借助 MessageChannel实现的,MessageChannel就是一个微任务, 那么为什么不用 setTimeout(0)呢,原因是递归执行 setTimeout(fn, 0) 时,最后间隔时间会变成 4 毫秒左右,而不是最初的 1 毫秒。我们知道浏览器一帧大概是16ms时间,4ms对于浏览器的一帧16ms时间过于浪费。

useRef

useRef 很简单啊!

官方这么说的:useRef() 和自己建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象, 类似于在 class 中使用实例(this)字段的方式(可以把其返回值看做一个全局变量,多次渲染指向始终不变);

所以,你明白了上面useRef 为什么能解闭包问题了吧,因为每次都返回了同一个对象(引用地址没有变)。

咱常常这么用:

const ref = React.useRef();
​
...

<div  ref={ref} >
​

用ref.current 取dom; 如果给React.useRef()传个参数,那么ref.current就指向这个参数

useMemo 和 useCallback

这两兄弟一起讲吧,他们很像, 都接收两个参数,第一个参数是回调函数,第二个参数是依赖数组。

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。 useMemo用来缓存对象(函数返回的对象, 依赖没有变,对象就不需要重新计算), 返回缓存对象。

useCallback用来缓存函数,返回缓存函数。

官方这么说的:如果你在渲染期间执行了高开销的计算,则可以使用 useMemo(或者 useCallback) 来进行优化。

注意哦,这两个只是优化方法,不要滥用哦。有的小友喜欢什么都包一下,个人觉得不合适,不合适,如果没有察觉不到性能问题,何必多此一举呢,按需取用比较好一点。当然如果你习惯包一下,那就包一下嘛,无可厚非,非要说的话,在内存消耗与渲染开销之间做一个平衡就好,我觉得这是一个见仁见智的问题。

一般这么用:

const fn = useCallback(()=>{...}, deps);

useMemo 原理

useMemo 和 useCallback 很像,我们仿一个就行了。

const hookStates = [];// 保存数据状态的数组,每个组件只有一个
let hookIndex = 0; // 索引
​
function useMemo(callback, deps) {
  if (hookStates[hookIndex]) {
    // 说明不是第一次
    let [laseMemo, lastDeps] = hookStates[hookIndex];
    // 判断一下依赖是否变化
    let same = deps.every((item, index) => item === lastDeps[index]);
    if (same) {
      hookIndex++;
      return laseMemo;
    } else {
      let newMemo = callback(); // 与callback 不同的地方
      hookStates[hookIndex++] = [newMemo, deps];
      return newMemo;
    }
  } else {
    // 初始化
    let newMemo = callback(); // 与useCallback 不同的地方
    hookStates[hookIndex++] = [newMemo, deps];
    return newMemo;
  }
}

useCallback 同理,只返回时直接返回索引值就好。

useLayoutEffect

这个,用的比较少。

官方是这么说的:

一个对用户可见的 DOM 变更就必须在浏览器执行下一次绘制前被同步执行,这样用户才不会感觉到视觉上的不一致。React 为此提供了一个额外的 useLayoutEffect Hook 来处理这类 effect。它和 useEffect 的结构相同,区别只是调用时机不同

useLayoutEffect 与 componentDidMount、componentDidUpdate 的调用阶段是一样的, 它会在所有的 DOM 变更之后同步调用 effect, 可以使用它来读取 DOM 布局并同步触发重渲染, 推荐你一开始先用 useEffect,只有当它出问题的时候再尝试使用 useLayoutEffect。

有一点点绕,说人话就是: useEffect在浏览器渲染结束后执行,useLayoutEffect 则是在dom更新后,浏览器绘制前执行。

useEffect不会阻塞浏览器渲染,是用宏任务实现的,而useLayoutEffect会阻塞浏览器渲染,在渲染前执行,是同步的,如果希望页面刷新尽可能快,可以用这个useLayoutEffect,比如拖拽。

useLayoutEffect 和 useEffect 原理差不多(就不写了),只不过useEffect 用宏任务,useLayoutEffect呢,react内部是通过同步代码去控制他的执行时机(我们高仿的话也可以用微任务,能得到同样的渲染效果,因为在一个事件环里,同步代码和微任务都是在浏览器画页面前执行) ,useEffect还多了个销毁步骤。

这里说到微任务,宏任务,就简单说一下浏览器的事件环吧:

主栈代码执行,遇到宏任务(定时器什么的),放进宏任务队列,遇见微任务(promise 什么的),放进微任务队列,主栈任务清空后,会清空微任务队列里的所有任务,然后js线程挂起,浏览器开始渲染页面(GUI 渲染线程和 JS 引擎线程是相互排斥的),之后再会取出一个宏任务在主栈里执行。如此循环。

useImperativeHandle 和 forwardRef

这个,用的也挺少。

官方说法: useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值, useImperativeHandle 应当与 forwardRef 一起使用。forwardRef会创建一个React组件,这个组件能够将其接受的ref属性转发到其组件树下的另一个组件中。

Ref 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递(换句话说,“转发”它)给子组件

解释补充一下吧:

类组件是有实例的,所以给类组件加ref属性,ref.current 就等于类实例,但是如果想给直接给函数组件增加ref属性就不行。这就需要forwardRef。forwardRef可以将ref从父组件转发到子组件的dom元素上,这样父组件就可以操作子组件元素,但父组件可以随意子组件元素,容易出问题。所以要用useImperativeHandle,限制父组件的能力。

咱一般这么用:

引入:

import React, {useRef, useImperativeHandle, forwardRef} from 'react';

父组件:

export default function App(){
  const  inputRef = useRef();
  return  <>
    <span onClick={()=>{inputRef.current.focus();}}>点我获得焦点</span>
    <WrapChild ref = {inputRef}/> // 传给子组件
  </>
}

子组件:


function Child(props, ref){ // 注意ref 是个形参哦,子组件第二个参数接收了ref
  const inputRef = useRef();
  // 在组件组件中用useImperativeHandle来限制父组件:
  useImperativeHandle(ref,()=>( // 这里做了ref 的关联
    {
      focus(){
        inputRef.current.focus();
      }
    }
  ));
  return <input ref = {inputRef}/>
}
const wrapChild = forwardRef(Child)
export default wrapChild;
​

这样父组件里的inputRef就只能调用focus方法。 在上述的示例中,React 会将 元素的 ref 作为第二个参数传递给 React.forwardRef 函数中的渲染函数。该渲染函数会将 ref 传递给 元素,因此,当 React 附加了 ref 属性之后,ref.current 将直接指向 DOM 元素实例。

useReducer

官方说: 在某些场景下, useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值(可以理解为复杂状态的放在一个地方统一维护)

可以这么理解: useState 是 useReducer 的简化版,语法糖。

主要用来做复杂状态的统一管理,比我们使用useState时,更希望这个状态纯粹简单,一般我们喜欢一个组件里,写很多useState。但有时项目里有一些复杂的状态,比如用户信息什么的,我们希望放在一块管理,可以用这个useReducer。总之维护复杂的状态可以考虑useReducer。

我们一般这么用:

const [state, dispatch] = useReducer(reducer, initialState);

上面是useReducer()的基本用法,它接受reducer函数和状态的初始值作为参数,返回一个数组,数组的第一个成员是状态的当前值,第二个成员是发送的action的dispatch函数。

举个栗子,下面是一个计数器的例子,用于计算状态的reducer函数如下:

const myReducer = (state, action) => {
  switch(action.type)  {
    case 'countUp':
      return  {
        ...state,
        count: state.count + 1
      }
    default:
      return  state;
  }
}

组件代码如下:

function App() {
  const [state, dispatch] = useReducer(myReducer, { count:   0 });
  return  (
    <div className="App">
      <button onClick={() => dispatch({ type: 'countUp' })}>
        +1
      </button>
      <p>Count: {state.count}</p>
    </div>
  );
}

由于 Hooks 可以提供共享状态和 Reducer 函数,所以它在某些方面可以取代 Redux。redux 是react 世界状态管理的扛把子,说到redux,就说到状态管理 ,那他是怎么取代Redux,来做简单项目的状态管理呢?我们先说说useContext这个hook,然后再回答这个问题。

useContext

官方说法:

const value = useContext(MyContext);

useContext(MyContext), 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。

当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 所在组件会触发重渲染, React.memo无法阻止这种渲染。

那咱们现在来用一下,先来最简单的:

const AppContext = React.createContext(); // 定义context;
​
<AppContext.Provider value={{
  username: 'superawesome'
}}>
  <div className="App">
    <Navbar/>
    <Messages/>
  </div>
</AppContext.Provider>
​

子级组件使用

const Navbar = () => {
    const {username} = useContext(AppContext);  // 获取context
    return (
    <div className="navbar">
      <p>AwesomeSite</p>
      <p>{username}</p>
    </div>
  );
}

当然,应该没人会这么用,太傻了,不符合实际业务场景,我们实际使用的使用时候,一般结合useReducer来用,可以用来做简单项目的状态管理。

做状态管理呢,我们至少需要三个东西,一个是我们的状态集(也就是数据),一个是用来改变状态的东西(更新视图的东西),还需要一个管理他们的东西。

举个栗子,就写个计数器吧,我们先定义一下数据管理文件,定义好context,和reducer:

import {createContext} from 'react';
​
export const CommonContext = createContext({}); 
​
export const CommonReducer = (state, action) => {
  switch(action.type){
    case 'increment':
      return { count: state.count+ 1};
    case 'decrease':
      return {count: state.count-1};
    default: 
      throw new Error();
  }
}

然后定义我们的父级组件,用上面定义的context传值,传出去dispatch函数(改变状态的东西),和state 数据(状态集):

import { CommonContext, CommonReducer } from './store';
import Child from './child';
​
export default function Counter() {
  const [state, dispatch] = useReducer(CommonReducer, {count: 3});
  return (
    <CommonContext.Provider value={{ dispatch, state }}>
        <Child />
    </CommonContext.Provider>
  );
}

我们父级组件在和最后一级组件之间,可能还有很多级组件,比如上面引入的./child文件是下面这样的:

import GrandSon1 from './grandson';
export default function C () {
  return (
    <div>
      <GrandSon/>
    </div>
  );
}

我们在最后一级组件里取值, 注意要从数据文件里引入同一个context:

import { CommonContext } from './store'
export default function Counter() {
  const { dispatch, state } = useContext(CommonContext);
  return (
    <div>
      <button onClick={() => dispatch({ type: 'decrease' })}>grandson-{state.count}</button>
    </div>
  );
}

这样一个简单的计数器就写好了。我们就可以用这个来做状态管理,做一些跨多级(平行)组件传递信息的事情。

但是,这里可能有一个问题,就是咱每次调用dispatch更新状态的时候,组件会从CommonContext.Provider开始都重新渲染一遍(如果把useReducer理解成一个加强版useState, 我们其实就是更新了根组件的状态,子组件必然会刷新)。

如果我们的项目简单,不需要频繁的这么传递信息,当然可以用这种方式,多一些渲染消耗我们也能接受,我们可以随便在哪里dispatch就好。

但是,如果项目复杂了,我们还这么搞,每次都从根组件开始更新渲染,无疑很让人头大,当然我们有各种方法去优化这种更新,比如用useMemo包一下组件,但是写业务的时候,总是背着这种要优化的包袱,让人束手束脚,不是一个好现象,我希望写大部分业务的时候,我可以闭着眼睛写也不会出错。所以,我们需要专业的状态管理工具。

想到react 专业的状态管理,我们首先会想到redux,但这个工具有点稍重,然后会想到mobx,嗯...篇幅有限,就不在这里继续展开了。

自定义hook

官方说法: 自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。

简单吧。

只要一个函数以use开头,并且里面调用了别的hook,那它就是一个自定义hook。

好了,既然你已经读完了全文,嗯,你现在已经是一个react hook小高手了。