react18学习笔记
为最新的 next.js 做点基础功课,学习 react18 的文档
记录一些对比之前不太一样的地方或者值得注意的地方
也会相对用的比较多的 Vue3,谈一些 React18 的理解
纯函数
react 特别强调函数要纯
不论是组件,还是对 state 的修改,一定要保持函数的纯粹性
https://zh-hans.react.dev/learn/keeping-components-pure
一个函数只做一件事情
输入相同,则输出相同
纯函数仅仅执行计算,因此调用它们两次不会改变任何东西
React 假设你编写的所有组件都是纯函数
不应该改变任何用于组件渲染的输入。这包括 props、state 和 context。通过 state 来更新界面,而不要改变预先存在的对象。
编写纯函数是编写 react 组件的前提条件
与函数的纯粹性相对的副作用,应该在 事件处理
或者 useEffect
中处理
严格模式
React 提供了 “严格模式”,在严格模式下开发时,它将会调用每个组件函数两次。通过重复调用组件函数,严格模式有助于找到违反这些规则的组件。
引入严格模式,可以用 <React.StrictMode>
包裹根组件。一些框架会默认这样做。
state 的快照理解和 Immer
下面这个例子,点击一次并不会使 number 的结果从 0 变成 3
1 | import { useState } from "react"; |
因为其等价于
1 | <button |
哪怕用 setTimeout 包裹其中一个 setTimeout 也不会影响结果:
1 | <h1>{number}</h1> // 0 -> 5 |
一个 state 变量的值永远不会在一次渲染的内部发生变化, 即使其事件处理函数的代码是异步的。
React 会使 state 的值始终“固定”在一次渲染的各个事件处理函数内部。
渲染期间改变 state 的值
将 setNumber
的参数写成一个 纯函数
:
1 | import { useState } from "react"; |
在一次渲染期间,会产生一个 state 的更新队列。例如上面的 state 更新队列可以表示为 [(n) => n + 1,(n) => n + 1,(n) => n + 1]
更新对象形式的 state
https://zh-hans.react.dev/learn/updating-objects-in-state#treat-state-as-read-only
如果 state 是一个对象,这个时候直接通过对象的引用关系去修改 state 是不正确的,这不会触发渲染
1 | const [position, setPosition] = useState({ x: 0, y: 0 }); |
正确的写法应该是
1 | setPosition({ |
如果对象复杂嵌套,这样书写也将变得非常繁琐,官方会推荐使用 Immer
使用 Immer
Immer 有点像 Vue3 中的 reactive
,其原理都是使用 Proxy
记录对 state 的修改,并创建出新的对象
安装:npm install use-immer
1 | const [position, updatePosition] = useImmer({ x: 0, y: 0 }); |
更新数组形式的 state
不论是数组还是对象,都要保持 state 的只读性
在使用setState
去更新数组的时候,应该避免使用会修改原数组的方法,比如 push,unshift,pop,shift,splice,splice,arr[i] = ...,reverse,sort
如果使用 useImmer 声明 state,则可以使用所有的方法
状态管理的原则
这一部分算是响应式框架通用的一些原则,遵守这些原则,在代码组织上会显得更加合理
设计组件的思路
https://zh-hans.react.dev/learn/reacting-to-input-with-state#thinking-about-ui-declaratively
- 分析组件所有可能存在的状态
- 确定哪些因素可以改变这些状态,人为输入 or 计算机输入
- 通过代码表示状态,
useState
或者ref
- 连接状态和事件处理函数
构建 state 的原则
合并相关联的 state
如果一个动作往往要改动两个 state,两个 state 又相互独立,那么可以把两个 state 用一个对象包裹起来,每次都修改这个对象
避免矛盾的 state
用 一个 state 表示 typing
sending
sent
比 三个 state 表示 isTyping
isSending
isSent
要更好
用尽可能少的 state
相对来说,组件状态越少,组件维护起来的成本就越小。避免冗余的和重复的 state
避免深度嵌套的 state
尽可能创建扁平化的 state
组件之间的 state
如果子组件之间有相互影响的 state,那么最好把它交给父组件管理
对于独特的状态,都应该有单一的数据源(state),这个 state 可以被传递,但是最好不要复制
组件内 state 的移除和保留
和 vue 一样,react 也有特殊的属性 key
,更新 key
就会强制重置组件
统一管理状态和动作: 编写 reducer
当组件的状态非常多的情况下,频繁地 setState 会让代码难以维护
如果能把更新状态的操作封装起来,用一个动作去表示,然后传入参数,就可以使引用更加清晰
reducer 是一个调度器,接收并执行不同的动作,返回新的结果
dispatch 用来发出动作,给出动作和计算新状态所需要的额外信息
要将状态设置逻辑从事件处理程序移到 reducer 函数中,你需要:
- 声明当前状态(tasks)作为第一个参数;
- 声明 action 对象作为第二个参数;
- 从 reducer 返回 下一个 状态(React 会将旧的状态设置为这个最新的状态)。
1 | function reducer(state, action) { |
使用 useReducer 替代 useState
https://zh-hans.react.dev/reference/react/useReducer#dispatch
1 | import { useReducer } from "react"; |
与 setState 相同,reducer 也必须是纯函数,在修改对象或者数组的状态时,也需要替换整个数组或者对象
可以用 useImmerReducer
替换 useReducer 来简化 reducer 的写法
使用 context 深层传递参数
React 的 context 特性让我想到了 vue 的 provide 和 inject
两者都是解决组件参数传递层级过深的问题,只是写法不一样而已
创建 context
1 | // 创建一个context, context.js |
父组件提供 context
1 | import { Context } from "context.js"; |
子组件接收 context
1 | import { useContext } from "react"; |
结合 useReducer 和 context,创建局部 store
https://zh-hans.react.dev/learn/scaling-up-with-reducer-and-context#combining-a-reducer-with-context
useReducer
可以将对数据的修改整理成 action
,通过 dispatch
一个动作更新状态
createContext
提供了一个局部共享的状态
将 useReducer
所提供的 state
和 dispatch函数
通过 context 传递给子组件,那么子组件就可以通过 action
修改父组件提供的状态了
这样做的好处是
- 将 reducer 封装组件,子组件不需要关心父组件动作的对状态的具体修改,只需要派发动作就可以修改父组件的状态
- 把 context 和 reducer 放在一起统一管理,使得负责构建 UI 的文件更加纯粹,视图和逻辑相分离
1 | // AppContext.jsx --------------------------- |
react 里的 useRef
在我理解中的 Vue 和 React 框架设计的区别是,Vue 是响应性数据驱动更新视图,而 React 是状态驱动更新视图。
Vue 中的响应性数据:ref()
和reactive()
声明的数据承载了表示状态的功能的同时也承载了可变性数据计算的功能。
React 的状态和可变性数据是分开的两个概念,state
只能由纯函数修改,并且具有不可变性,用来更新视图。ref
可以被随意修改,用来做与视图无关的计算
所以 React 中的ref
并不会像 Vue 中的那样触发视图更新,只能用来承载组件内的数据计算或者 dom 操作
1 | // 点击按钮后,打印结果会变成 1 ,但是视图依然是0 |
useRef 的 dom 操作
在这方面,react 和 vue 的差别不大
1 | import { useRef } from "react"; |
使用 flushSync 更新 state
flushSync 可以确保其回调函数内的更新立即反映到 dom 上
它和nextTick
解决的问题相似:视图的更新是异步的,某些情况下希望在操作数据之后立刻对新的 dom 进行操作
如果回调内有大量的 dom 更新,将会出现性能问题
处理渲染本身产生的副作用:Effect
组件的状态变更会引起渲染,渲染会引起一些副作用,有些时候需要处理渲染引起的副作用,这就会用到 useEffect
1 | import { useEffect } from "react"; |
useEffect
的回调函数会在渲染的提交阶段(也就是屏幕更新渲染之后)执行,使用 useEffect
需要显式指定依赖
开发环境中 Effect 的重复执行问题
重复执行 React 的调试行为,Effect 的双重执行有利于开发者检测自己的 Effect 是否有清理函数终止掉多余的副作用
Effect 常见用法
数据获取
1 | import { useEffect, useState } from "react"; |
订阅或监听事件
在组件挂载时注册事件监听器或订阅某些服务,并在组件卸载时取消订阅。这些操作包括 WebSocket 连接、Redux store 订阅、浏览器事件(如键盘或鼠标事件)等。
1 | import { useEffect } from "react"; |
定时器或者间隔
1 | import { useEffect, useState } from "react"; |
依赖变化
类似于 Vue 中的 watch
1 | import { useEffect, useState } from "react"; |
手动操作 dom
利用了 useEffect
在组件挂载之后执行一次的特性
不需要 Effect 的常见场景
https://zh-hans.react.dev/learn/you-might-not-need-an-effect
Effect 的生命周期
在 React 中,React 希望开发者将 Effect 的生命周期与组件的生命周期区分开来看待
组件的生命周期:
挂载 -> 更新 -> 卸载
Effect 的生命周期:
启动(Effect 函数执行) -> 依赖项更新(Effect 函数执行) -> 停止(清理函数执行)
即使没有依赖项,Effect 也会按照这个生命周期执行(仅执行一次,因为没有依赖项,自然也没有更新)
使用自定义 hook 抽离公共逻辑
Vue3 中也有类似的例子,在组件 setup 的时候,执行一些方法(同样约定以 use 开头),为组件提供一些预先封装好的功能
这是 React 给出的一个自定义 hook,用来给组件提供一个封装好 online
和 offline
事件并给出 isOnline
返回值的例子
1 | function useOnlineStatus() { |
hook 和组件
React 应用是由组件构成,而组件由内置或自定义 Hook 构成。
React 约定
React 组件名称必须以大写字母开头
Hook 的名称必须以use
开头,hook 可以返回任意值
关于 hook 的命名原则,是否所有渲染期间的调用函数都应该以 use 开头
使用 hook 封装的公共方法,应该被视为 hook,并以use
作以标识
hook 的内容
自定义 hook 虽然可以提供 state,但是其重点在于所封装的逻辑,同时必须保持每次调用 hook 的独立性
自定义 hook 必须是纯函数
自定义 Hook 共享的只是状态逻辑而不是状态本身。对 Hook 的每个调用完全独立于对同一个 Hook 的其他调用。
学习 react18 过程中的术语概念
state
& 快照
纯函数
& effect
ref
prop
& context
hook
reducer
& dispatch
& action