Loading [MathJax]/jax/input/TeX/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >React Hook实践总结

React Hook实践总结

原创
作者头像
jadeCarver
修改于 2021-01-08 12:19:55
修改于 2021-01-08 12:19:55
1.1K0
举报
文章被收录于专栏:CS成长之路CS成长之路

最近一年几乎都在使用 TypeScript + Hooks 编写函数式组件,这一篇是我使用 hooks 的一些总结。

开始之前,看一个经典的计数器例子:

代码语言:txt
AI代码解释
复制
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>点击了 {count} 次</p>
      <button onClick={() => setCount(count + 1)}>
        Click
      </button>
    </div>
  );
}
每一帧都是独立的

任意一次渲染中的count常量都不会随着时间改变。渲染输出会变是因为我们的组件被一次次调用,而每一次调用引起的渲染中,它包含的count值独立于其他渲染。 —— Dan Abramov

在React组件中,通过改变状态来触发组件的重新 render,每次渲染都可以理解为一。在每一帧中,状态只是一个普通的变量,render的时候,它都是独立不变的。

也就是说,在每次渲染中,所有的 state、props 以及 effects 在组件的任意位置都是固定的,我们无法直接获取过去或者未来渲染周期的状态。

state 变化,引发了视图的更新,从直觉上看来,这里是不是使用了数据绑定或者,观察者之类的高级技巧,实际上不是的,它只是函数的重复调用而已,count 是每次调用都独立的局部变量。

更新 state

在react中,state或者props的改变,都会触发重新渲染。函数式组件以参数的形式接受props,props变化,整个组件都会重新渲染。useState在函数式组件内部创建了状态,并提供了一个改变状态的方法。

代码语言:txt
AI代码解释
复制
const [count, setCount] = useState(0);

几个值得注意的点:useState的初始值可以是一个简单类型,也可以是复杂类型。同时它还可以接收一个函数,将函数的返回值作为该state的初始值。

代码语言:txt
AI代码解释
复制
const a = 1;
const b = 2;
const [count, setCount] = useState(() => a + b);

既然每一帧的渲染中,state 都是独立的,其实就会有一个问题,当我们执行完 setCount 之后,并不会立即拿到最新的 count 的值:

代码语言:txt
AI代码解释
复制
const [count, setCount] = useState(0);
setCount(count + 1);
console.log(count); // 0

也就是说,count 的值在本次渲染周期内是固定不变的,直到下一次渲染,count 才会更新为 1.这也是为什么感觉 state 的改变是异步的原因。

获取未来或者过去的state

如果想要获取到最新的state值,则可以通过给setCount方法传入一个函数来执行。

还有一种方法就是使用 useRef,它是一个所有帧共享的变量,你可以在任何时间改变它,然后在它未来的帧中访问它。也就是说,useRef可以为渲染视图的特定一帧打一个快照进行保存。

什么样的数据需要保存为内部 state

在实际使用中,一个组件可能会出现大量的 useState定义,这个时候,我们需要回头反思,如此多的 state 定义是否有必要?

我们知道,react 状态的变化会引发视图的更新,所以将一个变量定义为 state 的标准是:它的改变需要直接引发视图的更新?如果答案是否定的,那就完全不必定义一个 state 出来,而是通过一般的变量将其缓存起来。或者说,使用 useRef是一种不错的选择。

管理复杂状态的两种选择: useReducer + useContext

对于一些需要全局使用的状态,如果需要在多层组件之间传递更新数据,这很容易造成逻辑混乱且不可追踪,则可以通过 useContext 来简化这一过程,以避免显示地在每一层组件之间传递props,子组件可以在任何地方访问到该 context 的值。在下面的例子中,我们将终端的平台和版本通过context注入:

代码语言:txt
AI代码解释
复制
const client = {
  mobile: {
    system: 'android',
    version: '8.0.0'
  },
  mac: {
    system: 'MacOS',
    version: '11.0.1'
  }
}
const ClientContext = React.CreateContext({});
const App = () => {
  return (
  		<ClientContext.Provider value={client.mac}>
    		<MyComponent />
    </ClientContext.Provider>
  )
}

const MyComponent = () => {
  const client = useContext(ClientContext);
  return <>当前系统为{client.system},系统版本为{client.version}</>
}

在某一个节点注入 context,该组件及其所有下属组件都会共享这个 context。当该 context 的值发生变化时,其下的所有组件都会重新 render.

useReducer,是改变 state 的另一种方式。顾名思义,就是 reducer 的 hooks 用法。reducer 接受一个改变 state 的方法,以及触发方法的 action,计算之后,返回新的 state.类似于这样 (state, action) => newState.useReducer在某些复杂场景下比 useState更实用,例如一个操作会引发N多个 state 的更新,或者说,state 本身嵌套很多层,更新的逻辑易遗漏,维护起来一片凌乱等等场景。

reducer 是一个纯函数,也就是说,它不包含任何 UI 和副作用操作。也就是说,只要输入的值不变,其输出的值也不会改变。

同样地,reducer 中的数据是 immutable 的,不要直接改变输入的 state,而是应该返回一个新的改变后的 state.

action 是一个用 type 标识的动作,例如对计数器的 increase、decrease等,在 reducer中,可以根据 action type 的不同,采用不同的数据处理。同时,它可以接收第二个参数 payload,传入执行该 action 需要的额外数据。

代码语言:txt
AI代码解释
复制
const [state, dispatch] = useReducer(reducer, initialArg, init);

useReducer接收一个 reducer 函数,以及一个初始的 state 值,暴露出计算之后的新 state,以及一个 dispatch 方法,它接收一个 action 为参数,用来触发相应的 reducer. 下面使用 useReducer重构计数器的例子:

代码语言:txt
AI代码解释
复制
const initialCount = {
  count: 0
};

const countReducer = (state, action) => {
  switch (action.type) {
    case "increase":
      return { count: state.count + 1 };
    case "decrease":
      return { count: state.count - 1 };
    case "reset":
      return { count: action.payload };
    default:
      return initialCount;
  }
};

export default function App() {
  const [state, dispatch] = useReducer(countReducer, initialCount);

  return (
    <div className="App">
      <h1>{state.count}</h1>
      <button onClick={() => dispatch({ type: "increase" })}>+1</button>
      <button onClick={() => dispatch({ type: "decrease" })}>-1</button>
      <button
        onClick={() => dispatch({ type: "reset", payload: initialCount.count })}
      >
        reset
      </button>
    </div>
  );
}

在某些复杂的场景中,reducer 的引入实际上将复杂的 state 更新行为剥离出来,单独在 reducer 之中维护,而组件的核心交互逻辑我们只需要关照 dispatch 了哪个 action,这样使得代码可读性大大提高,组件的核心逻辑也会清晰明了。同时,对于不涉及多层组件交互的状态,并不适合使用 reducer 来维护,这样,反而增加了维护的复杂度。

在一些复杂场景下,结合 useContextuseReducer可以发挥出十分强大的威力。一般的做法是,可以把 state 和 dispatch 方法通过 context 注入,这样,很方便地实现了状态的集中管理和更新。这种方法最好不要滥用,因为集中管理、处处可以变更的方式虽然看起来方便很多,但在 context 的作用范围处处都可以通过 dispatch 来更新 state,这样很容易造成 state 的更新不可追踪。

一般情况下,这种模式适合多层组件状态交互十分密集,且数据具有较好的完整性和共享需要,整个 state 描述的是同一件事,而不是把任意的数据都塞进去维护,这样写起来一时爽,维护起来火葬场~

副作用管理

useEffectuseLayoutEffect都是用来执行视图之外的副作用。前者在每次状态更新且视图也渲染完毕之后执行。后者则是在DOM更新完毕,但页面渲染之前执行,所以会阻塞页面渲染。

如前所述,在每一帧的渲染中,useEffect 中使用的 state 和 props 也是独立不变的。

可以通过给它传入第二个参数,一个依赖数组,来跳过不必要的副作用操作,React 通过对比依赖数组的值是否发生变化来决定是否执行副作用函数。当第二个参数为一个空数组的时候,意味着这个 Effects 只会执行一次。

对于依赖数组,使用不当经常会遇到各种各样的重复渲染的情况。不要添加不必要的依赖在数组中,因为依赖项越多,意味着该 Effects 被多次执行的概览越大。例如,在下面的场景中,如果需要在 Effects 中更新 state,不必将该 state 传入依赖数组,而是通过给 setCount 传入回调的方式去获得当前 state:

代码语言:txt
AI代码解释
复制
useEffect(() => {
  // setCount(count + 1); 这种方式会引入一个 state 的依赖项。
  setCount(count => count + 1);
}, [])

在React官方的文档中,还提到了两种需要避免重复渲染的情况及处理方式:

  1. 当依赖项中传入一个函数时,通过使用 useCallback来包裹函数避免函数反复被创建;
  2. 当依赖项中传入数组或者对象等引用类型,通过使用 useMemo来缓存处理它。
使用useMemo和useCallback

如上所述,合理地使用 useMemouseCallback能够避免不必要的渲染。当对象或者数组作为 props 传入的时候,可以使用 useMemo来缓存对象,使其在必要的时候更新:

代码语言:txt
AI代码解释
复制
const data = useMemo(() => ({ id}), [id]);

<ComponentA data={data} />

只要 id不变,Component 就不会重新渲染。

useMemo 同样可以用来缓存组件内部的部分 React Element ,以避免重复渲染并没有变化的部分。

使用 useMemo 或者 useCallback 并不是绝对会提升性能。如果你缓存的数据永远不会改变或者说,每一次都会改变,那大可不必使用这两个 hooks,毕竟它们需要额外的计算成本以及存储空间,有的时候得不偿失。

最后,在React哲学一文中,官方给出了一种使用 React 来构建应用的思路,我觉得十分赞。这篇文章中提到,开始的时候,找出应用所需的最小集合,其他数组均有它们计算而出。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
2018年第九届Java B组蓝桥杯省赛真题
题目描述 2000年的1月1日,是那一年的第1天。 那么,2000年的5月4日,是那一年的第几天?
Max超
2020/12/23
8460
2018年第九届Java B组蓝桥杯省赛真题
第十一届蓝桥杯大赛个人赛校内选拔(软件类)题解
第十一届蓝桥杯大赛个人赛校内选拔(软件类)题目:https://blog.csdn.net/qq262593421/article/details/111598726
静谧星空TEL
2021/04/27
1K0
第十一届蓝桥杯大赛个人赛校内选拔(软件类)题解
2018 年蓝桥杯B组 递增三元组--------------C语言—菜鸟级
给定三个整数数组 A = [A1, A2, … AN], B = [B1, B2, … BN], C = [C1, C2, … CN], 请你统计有多少个三元组(i, j, k) 满足:
Fivecc
2022/11/21
2820
第十三届蓝桥杯省赛JavaC组真题——详细答案对照(完整版)
话说真的对于大专生来说已经是非常难的了呢,能拿到省一的基本上都是万里挑一的孩子呢。
红目香薰
2023/02/10
8190
第十三届蓝桥杯省赛JavaC组真题——详细答案对照(完整版)
第八届蓝桥杯B组Java类真题 第九题 标题: 分巧克力
    儿童节那天有K位小朋友到小明家做客。小明拿出了珍藏的巧克力招待小朋友们。     小明一共有N块巧克力,其中第i块是Hi x Wi的方格组成的长方形。
静谧星空TEL
2022/05/07
2730
第八届蓝桥杯B组Java类真题 第九题 标题: 分巧克力
第九届蓝桥杯省赛JavaC组真题——详细答案对照(完整版)
目录 A、哪天返回 B、猴子分香蕉 C、字母阵列 D、第几个幸运数 E、书号验证 F、打印大X G、缩位求和 H、等腰三角形 I、小朋友崇拜圈 J、耐摔指数 A、哪天返回 小明被不明势力劫持。后被扔到x星站再无问津。小明得知每天都有飞船飞往地球,但需要108元的船票,而他却身无分文。 他决定在x星战打工。好心的老板答应包食宿,第1天给他1元钱。 并且,以后的每一天都比前一天多2元钱,直到他有足够的钱买票。 请计算一下,小明在第几天就能凑够108元,返回地球。 题解: package actio
红目香薰
2022/11/29
1.3K0
第九届蓝桥杯省赛JavaC组真题——详细答案对照(完整版)
第九届蓝桥杯大赛个人赛省赛(软件类)Java类 真题 第五题 标题:全球变暖
问题描述  标题:全球变暖 你有一张某海域NxN像素的照片,"."表示海洋、"#"表示陆地,如下所示: ....... .##.... .##.... ....##. ..####. ...###. ....... 其中"上下左右"四个方向上连在一起的一片陆地组成一座岛屿。例如上图就有2座岛屿。   由于全球变暖导致了海面上升,科学家预测未来几十年,岛屿边缘一个像素的范围会被海水淹没。具体来说如果一块陆地像素与海洋相邻(上下左右四个相邻像素中有海洋),它就会被淹没。   例如上
静谧星空TEL
2021/04/27
3890
第九届蓝桥杯大赛个人赛省赛(软件类)Java类 真题 第五题 标题:全球变暖
2018年第九届C/C++ B组蓝桥杯省赛真题
题目描述 2000年的1月1日,是那一年的第1天。 那么,2000年的5月4日,是那一年的第几天? 注意:需要提交的是一个整数,不要填写任何多余内容。
Max超
2020/12/17
4830
2018年第九届C/C++ B组蓝桥杯省赛真题
第六届蓝桥杯JavaC组省赛真题——详细答案对照(包含垒骰子)
Excel表的格子很多,为了避免把某行的数据和相邻行混淆,可以采用隔行变色的样式。 小明设计的样式为:第1行蓝色,第2行白色,第3行蓝色,第4行白色,.... 现在小明想知道,从第21行到第50行一共包含了多少个蓝色的行。
红目香薰
2022/11/29
5860
第六届蓝桥杯JavaC组省赛真题——详细答案对照(包含垒骰子)
第八届蓝桥杯省赛javaB组题目解析
作者自己做完之后发现省赛的一幕其实是不难的,说实话,自己觉得题目难度还没有PAT甲级的难度高。 而且作者做了这么些天之后发现了,PAT甲级主要喜欢考数据结构方面的知识,而蓝桥杯则喜欢考算法这一类的,但是蓝桥杯的算法题目有些又不是很正规,因为作者有好些题目都是通过暴力求解的,关键是这样还过了,就很不可思议,和我想象中的算法比赛卡时间有点不太一样,说了这么多,这些只代表作者自己的一些看法,如有不同,欢迎在下面评论留言交流。 第一题纯粹就是算术,送分题,这里作者就不讲解了 第二题: 标题:纸牌三角形
萌萌哒的瓤瓤
2020/08/26
5390
第八届蓝桥杯省赛javaB组题目解析
第十届蓝桥杯省赛JavaC组真题——详细答案对照(完整版-包含打扫机器人的视频全过程讲解与编码内容对照)
走廊内部署了 K 台扫地机器人,其中第 i 台在第 Ai 个方格区域中。 已知扫地机器人每分钟可以移动到左右相邻的方格中,并将该区域清扫干 净。 请你编写一个程序,计算每台机器人的清扫路线,使得 1. 它们最终都返回出发方格, 2. 每个方格区域都至少被清扫一遍, 3. 从机器人开始行动到最后一台机器人归位花费的时间最少。 注意多台机器人可以同时清扫同一方块区域,它们不会互相影响。 输出最少花费的时间。 在上图所示的例子中,最少花费时间是 6。第一台路线:2-1-2-3-4-3-2,清 扫了 1、2、3、4 号区域。第二台路线 5-6-7-6-5,清扫了 5、6、7。第三台路线 10-9-8-9-10,清扫了 8、9 和 10。 【输入格式】 第一行包含两个整数 N 和 K。 接下来 K 行,每行一个整数 Ai。  案例: 输入: 10 3 3 5 8 输出: 6
红目香薰
2022/11/29
4430
第十届蓝桥杯省赛JavaC组真题——详细答案对照(完整版-包含打扫机器人的视频全过程讲解与编码内容对照)
第十一届蓝桥杯省赛JavaC组真题——详细答案对照(完整版)
该片段中从A = A + 4 所在的行到 A = A + 8 所在的行都在第一行的循环两次中。 REPEAT 6: 所在的行到 A = A + 7 所在的行都在 REPEAT 5: 循环中。 A = A + 5 实际总共的循环次数是 2 × 5 × 6 = 60 次。 请问该程序执行完毕之后,A 的值是多少? 【答案提交】 这是一道结果填空题,你只需要算出结果后提交即可。本题的结果为一个 整数,在提交答案时只填写这个整数,填写多余的内容将无法得分。
红目香薰
2022/11/29
6060
第十一届蓝桥杯省赛JavaC组真题——详细答案对照(完整版)
第九届蓝桥杯大赛个人赛省赛(软件类)Java真题第七题 标题:螺旋折线
 1、先将四个顶点的值算出来,d1、d2、d3、d4中的dot值分别代表四个象限的顶点
静谧星空TEL
2022/05/07
2490
第九届蓝桥杯大赛个人赛省赛(软件类)Java真题第七题 标题:螺旋折线
第十二届蓝桥杯省赛JavaC组【第一场】真题——详细答案对照(完整版)
【问题描述】已知大写字母 A 的 ASCII 码为 65,请问大写字母 L 的 ASCII 码是多少? 76 代码实现
红目香薰
2022/11/29
5330
第十二届蓝桥杯省赛JavaC组【第二场】真题——详细答案对照(完整版)
IEEE 754 规定一个双精度浮点数由 1位符号位、11 位阶和 52 位尾数组成(以上位数都表示二进制位数)。 请问,按此规定一个双精度浮点数占用几个字节?
红目香薰
2022/11/29
4860
第十二届蓝桥杯省赛JavaC组【第二场】真题——详细答案对照(完整版)
蓝桥杯 ——省赛题(java 组)
1.成绩分析 成绩分析成绩分析 题目: 小蓝给学生们组织了一场考试,卷面总分为 100 分,每个学生的得分都是一个 0 到 100 的整数。 请计算这次考试的最高分、最低分和平均分。 代码附上:  方法1: import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); /
王同学要努力
2022/12/21
3630
蓝桥杯 ——省赛题(java 组)
【蓝桥杯省赛】冲刺练习题【数组】倒计时【13】天
  编写一个程序,输入10个整数(为100以内非负整数)存入一维数组,按逆序重新存放后再输出。
红目香薰
2022/11/29
2620
【蓝桥杯省赛】冲刺练习题【数组】倒计时【13】天
2013第四届蓝桥杯Java组省赛题解析
2013第四届蓝桥杯Java组省赛题解析 目录 第一题:高斯日记 第二题:马虎的算式 第三题:第39级台阶 第四题:黄金连分数 ​第五题:前缀判断 第六题:三部排序 ​第七题:错误票据 第八题:翻硬币 第九题:带分数 第十题:连号区间数 第一题:高斯日记 大数学家高斯有个好习惯:无论如何都要记日记。 他的日记有个与众不同的地方,他从不注明年月日,而是用一个整数代替,比如:4210 后来人们知道,那个整数就是日期,它表示那一天是高斯出生后的第几天。这或许也是个好习惯,它时时刻刻提醒着主人:日子又过去一天
红目香薰
2022/11/29
3500
2013第四届蓝桥杯Java组省赛题解析
第十届蓝桥杯大赛软件类省赛 Java 大学 B组 试题G:外卖店优先级
问题描述 代码实现 import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.u
静谧星空TEL
2021/04/27
2890
第十届蓝桥杯大赛软件类省赛 Java 大学 B组 试题G:外卖店优先级
第十二届蓝桥杯决赛JavaC组真题——详细答案对照(全网唯一:异或变换100%数据)
  用 8 位二进制(一个字节)来表示一个非负整数,表示的最小值是 0 ,则一般能表示的最大值是多少?
红目香薰
2022/11/29
5220
第十二届蓝桥杯决赛JavaC组真题——详细答案对照(全网唯一:异或变换100%数据)
推荐阅读
2018年第九届Java B组蓝桥杯省赛真题
8460
第十一届蓝桥杯大赛个人赛校内选拔(软件类)题解
1K0
2018 年蓝桥杯B组 递增三元组--------------C语言—菜鸟级
2820
第十三届蓝桥杯省赛JavaC组真题——详细答案对照(完整版)
8190
第八届蓝桥杯B组Java类真题 第九题 标题: 分巧克力
2730
第九届蓝桥杯省赛JavaC组真题——详细答案对照(完整版)
1.3K0
第九届蓝桥杯大赛个人赛省赛(软件类)Java类 真题 第五题 标题:全球变暖
3890
2018年第九届C/C++ B组蓝桥杯省赛真题
4830
第六届蓝桥杯JavaC组省赛真题——详细答案对照(包含垒骰子)
5860
第八届蓝桥杯省赛javaB组题目解析
5390
第十届蓝桥杯省赛JavaC组真题——详细答案对照(完整版-包含打扫机器人的视频全过程讲解与编码内容对照)
4430
第十一届蓝桥杯省赛JavaC组真题——详细答案对照(完整版)
6060
第九届蓝桥杯大赛个人赛省赛(软件类)Java真题第七题 标题:螺旋折线
2490
第十二届蓝桥杯省赛JavaC组【第一场】真题——详细答案对照(完整版)
5330
第十二届蓝桥杯省赛JavaC组【第二场】真题——详细答案对照(完整版)
4860
蓝桥杯 ——省赛题(java 组)
3630
【蓝桥杯省赛】冲刺练习题【数组】倒计时【13】天
2620
2013第四届蓝桥杯Java组省赛题解析
3500
第十届蓝桥杯大赛软件类省赛 Java 大学 B组 试题G:外卖店优先级
2890
第十二届蓝桥杯决赛JavaC组真题——详细答案对照(全网唯一:异或变换100%数据)
5220
相关推荐
2018年第九届Java B组蓝桥杯省赛真题
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档