author 张向强
马上2022了,才开始说hooks,是不是有点晚?
不晚,不晚,总有新的小友,对它不那么熟悉,那这一篇从零开始,简洁有力的文章一定会很有帮助。
理论指导实践,有了理论依据我们才能跑的飞快,为了不误人子弟,我又重读了一遍官方文档(冗长而琐碎需要耐心的文档),所以文内最重要的理论依据会来源于官方文档摘抄(我会标识出来),请放心食用。
我希望你最好有一点react基础,一点点就够了,差不多就是能用react画个页面,然后知道一些生命周期的程度。
好了,闲话少说,我们开始。
Hooks 出现的背景
官方是这么介绍hooks的,Hook 是 React 16.8 的新增特性,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
意思就是,hooks让你可以写函数组件,让函数组件的能力像类组件一样强大。
它出现的原因,简单来说有这么三个原因(官方写的有点绕,我用人话说一遍):
-
是在类组件之间复用状态逻辑很难(类组件之前一般使用render props和高阶组件的方式来复用逻辑)。Hook 使你在无需修改组件结构的情况下复用状态逻辑
-
复杂的类组件会变的很难理解。比如,很多时候相互关联的逻辑要同时写在componentDidMount,componentDidUpdate,componentWillUnmount几个生命周期中,逻辑一旦复杂起来维护就很麻烦。再比如,我常常会在一个生命周期里写很多不相互关联的逻辑。 我们希望的是,相关的逻辑写一起,不相关的就分开写。但是之前我们只能写在react暴露的生命周期里,业务逻辑一旦堆起来,就变的又杂糅又分散,这也是无奈之举。为了解决这个问题,Hook 将组件中相互关联的部分拆分成更小的函数,(也就是相关的逻辑可以写在同一个hook里),而并非强制按照生命周期划分。
-
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小高手了。