React Hooks 小记(七)_useReducer

useReducer

usereducer 相当于 复杂的 useState
状态更新逻辑较复杂时可以考虑使用 useReducer。useReducer 可以同时更新多个状态,而且能把对状态的修改从组件中独立出来。

相比于 useState,useReducer 可以更好的描述“如何更新状态”。例如:组件负责发出行为,useReducer 负责更新状态。

好处是:让代码逻辑更清晰,代码行为更易预测

1. useReducer 的语法格式

useReducer 的基础语法如下:

const [state, dispatch] = useReducer(reducer, initState, initAction?)

其中:

1. reducer 是一个函数,类似于 (prevState, action) => newState。形参 prevState 表示旧状态,形参 action 表示本次的行为,返回值 newState 表示处理完毕后的新状态。

2. initState 表示初始状态,也就是默认值。

3. initAction 是进行状态初始化时候的处理函数,它是可选的,如果提供了 initAction 函数,则会把 initState 传递给 initAction 函数进行处理,initAction 的返回值会被当做初始状态。

4. 返回值 state 是状态值。dispatch 是更新 state 的方法,让他接收 action 作为参数,useReducer 只需要调用 dispatch(action) 方法传入的 action 即可更新 state。

2. 定义组件的基础结构

定义名为 Father 的父组件如下:

import React from 'react'// 父组件
export const Father: React.FC = () => {return (<div><button>修改 name 的值</button><div className="father"><Son1 /><Son2 /></div></div>)
}

定义名为 Son1 和 Son2 的两个子组件如下:

// 子组件1
const Son1: React.FC = () => {return <div className="son1"></div>
}// 子组件2
const Son2: React.FC = () => {return <div className="son2"></div>
}

在 index.css 中添加对应的样式:

.father {display: flex;justify-content: space-between;width: 100vw;
}.son1 {background-color: orange;min-height: 300px;flex: 1;padding: 10px;
}.son2 {background-color: lightblue;min-height: 300px;flex: 1;padding: 10px;
}

3. 定义 useReducer 的基础结构

1、按需导入 useReducer 函数:

import React, { useReducer } from 'react'

2、定义初始数据

const defaultState = { name: 'liulongbin', age: 16 }

3、定义 reducer 函数,它的作用是:根据旧状态,进行一系列处理,最终返回新状态

// 第一个参数永远是上一次的旧状态
const reducer = (prevState) => {// 首次进入页面 不会触发 reducer 函数执行console.log('触发了 reducer 函数')// 必须向外返回一个处理好的新状态 return prevState
}

4、在 Father 组件中,调用 useReducer(reducerFn, 初始状态) 函数,并得到 reducer 返回的状态:

// 父组件
export const Father: React.FC = () => {// useReducer(fn, 初始数据, 对初始数据进行处理的fn)const [state] = useReducer(reducer, defaultState)console.log(state)return (<div><button>修改 name 的值</button><div className="father"><Son1 /><Son2 /></div></div>)
}

5、为 reducer 中的 initState 指定数据类型:

// 定义状态的数据类型
type UserType = typeof defaultStateconst defaultState = { name: 'liulongbin', age: 16 }// 给 initState 指定类型为 UserType
const reducer = (prevState: UserType) => {console.log('触发了 reducer 函数')return prevState
}

6、接下来,在 Father 组件中使用 state 时,就可以出现类型的智能提示啦:

// 父组件
export const Father: React.FC = () => {const [state] = useReducer(reducer, defaultState)console.log(state.name, state.age)return (<div><button>修改 name 的值</button><div className="father"><Son1 /><Son2 /></div></div>)
}

4. 使用 initAction 处理初始数据

定义名为 initAction 的处理函数,如果初始数据中的 age 为小数、负数、或 0 时,对 age 进行非法值的处理:

const initAction = (initState: UserType) => {// 把 return 的对象,作为 useReducer 的初始值return { ...initState, age: Math.round(Math.abs(initState.age)) || 18 }
}

Father 组件中,使用步骤1声明的 initAction 函数如下:

// 父组件
export const Father: React.FC = () => {// useReducer(fn, 初始数据, 对初始数据进行处理的fn)const [state] = useReducer(reducer, defaultState, initAction)// 省略其它代码...
}

在定义 defaultState 时,为 age 提供非法值,可以看到非法值在 initAction 中被处理掉了。

5. 在 Father 组件中点击按钮修改 name 的值

1. 错误示范:

不要像 vue 响应式数据一样,直接通过 state.name = 'escook' 去修改

// 父组件
export const Father: React.FC = () => {// useReducer(fn, 初始数据, 对初始数据进行处理的fn)const [state] = useReducer(reducer, defaultState, initAction)console.log(state)const onChangeName = () => {// 注意:这种用法是错误的,因为不能【直接修改 state 的值】// 因为存储在 useReducer 中的数据都是“不可变”的!// 要想修改 useReducer 中的数据,必须触发 【reducer】 函数的重新计算,// 根据 reducer 形参中的旧状态对象(initState),经过一系列处理,返回一个“全新的”状态对象state.name = 'escook'}return (<div><button onClick={onChangeName}>修改 name 的值</button><div className="father"><Son1 /><Son2 /></div></div>)
}

2. 正确的操作

为了能够触发 reducer 函数的重新执行,我们需要在调用 useReducer() 后接收返回的 dispatch 函数。示例代码如下:

// Father 父组件 
const [state, dispatch] = useReducer(reducer, defaultState, initAction)

在 button 按钮的点击事件处理函数中,调用 dispatch() 函数,从而触发 reducer 函数的重新计算:

// Father 父组件 
const onChangeName = () => { dispatch() 
}

点击 Father 组件中如下的 button 按钮:

<button onClick={onChangeName}>修改 name 的值</button>

会触发 reducer 函数的重新执行,并打印 reducer 中的 console.log(),代码如下:

const reducer = (prevState: UserType) => {console.log('触发了 reducer 函数')return prevState
}

3. 调用 dispatch 传递参数给 reducer

在 Father 父组件按钮的点击事件处理函数 onChangeName 中,调用 dispatch() 函数并把参数传递给 reducer 的第2个形参,代码如下:

const onChangeName = () => {// 注意:参数的格式为 { type, payload? }// 其中:// type 的值是一个唯一的标识符,用来【指定本次操作的类型】,一般为大写的字符串// payload 是本次操作需要用到的数据,为可选参数。在这里,payload 指的是把用户名改为字符串 '刘龙彬'dispatch({type: 'UPDATE_NAME', payload: '刘龙彬'})
}

修改 reducer 函数的形参,添加名为 action 的第2个形参,用来接收 dispatch 传递过来的数据

const reducer = (prevState: UserType, action) => {// 打印 action 的值,终端显示的值为:// {type: 'UPDATE_NAME', payload: '刘龙彬'}console.log('触发了 reducer 函数', action)return prevState
}

在 reducer 中,根据接收到的 action.type 标识符,决定进行怎样的更新操作,最终 return 一个计算好的新状态。示例代码如下:

const reducer = (prevState: UserType, action) => {console.log('触发了 reducer 函数', action)// return prevStateswitch (action.type) {// 如果标识符是字符串 'UPDATE_NAME',则把用户名更新成 action.payload 的值// 最后,一定要返回一个新状态,因为 useReducer 中每一次的状态都是“不可变的”case 'UPDATE_NAME':return { ...prevState, name: action.payload }   // 解除引用,赋予新对象// 兜底操作:// 如果没有匹配到任何操作,则默认返回上一次的旧状态default:return prevState}
}

4、为 action 指定类型

在上述的 switch...case... 代码期间,没有 TS 的类型提示,这在大型项目中是致命的。因此,我们需要为 reducer 函数的第2个形参 action 指定操作的类型:

// 1. 定义 action 的类型
type ActionType = { type: 'UPDATE_NAME'; payload: string }// 2. 为 action 指定类型为 ActionType
const reducer = (prevState: UserType, action: ActionType) => {console.log('触发了 reducer 函数', action)// 3. 删掉之前的代码,再重复编写这段逻辑的时候,会出现 TS 的类型提示,非常 Niceswitch (action.type) {case 'UPDATE_NAME':return { ...prevState, name: action.payload }default:return prevState}
}

同时,在 Father 组件的 onChangeName 处理函数内,调用 dispatch() 时也有了类型提示

const onChangeName = () => {dispatch({ type: 'UPDATE_NAME', payload: '刘龙彬' })
}

注意:在今后的开发中,正确的顺序是

​ 1、先定义 ActionType 的类型

​ 2、修改 reducer 中的 switch...case... 逻辑

​ 3、在组件中调用 dispatch() 函数!这样能够充分利用 TS 的类型提示。

6. 把用户信息渲染到子组件中

1、在 Father 父组件中,通过jsx展开语法state 数据对象绑定为 Son1Son2props 属性:

// 父组件
export const Father: React.FC = () => {const [state, dispatch] = useReducer(reducer, defaultState, initAction)const onChangeName = () => {dispatch({ type: 'UPDATE_NAME', payload: '刘龙彬' })}return (<div><button onClick={onChangeName}>修改 name 的值</button><div className="father"><!-- 通过 props 的数据绑定,把数据传递给子组件 --><Son1 {...state} /><Son2 {...state} /></div></div>)
}

2、在子组件中,指定 props 的类型为 React.FC<UserType>,并使用 props 接收和渲染数据:

// 子组件1
const Son1: React.FC<UserType> = (props) => {return (<div className="son1"><p>用户信息:</p><p>{JSON.stringify(props)}</p></div>)
}// 子组件2
const Son2: React.FC<UserType> = (props) => {return (<div className="son2"><p>用户信息:</p><p>{JSON.stringify(props)}</p></div>)
}

修改完成后,点击父组件中的 button 按钮修改用户名,我们发现两个子组件中的数据同步发生了变化。

7. 在子组件中实现点击按钮 age 自增操作

1、扩充 ActionType 的类型如下:

// 定义 action 的类型 
type ActionType = { type: 'UPDATE_NAME'; payload: string } | { type: 'INCREMENT'; payload: number }

2、在 reducer 中添加 INCREMENTcase 匹配:

const reducer = (prevState: UserType, action: ActionType) => {console.log('触发了 reducer 函数', action)switch (action.type) {case 'UPDATE_NAME':return { ...prevState, name: action.payload }// 添加 INCREMENT 的 case 匹配case 'INCREMENT':return { ...prevState, age: prevState.age + action.payload }default:return prevState}
}

3、在子组件 Son1 中添加 +1button 按钮,并绑定点击事件处理函数:

// 子组件1
const Son1: React.FC<UserType> = (props) => {const add = () => {}return (<div className="son1"><p>用户信息:</p><p>{JSON.stringify(props)}</p><button onClick={add}>+1</button></div>)
}

4、现在的问题是:子组件 Son1 中无法调用到父组件的 dispatch 函数。

为了解决这个问题,我们需要在 Father 父组件中,通过 props 把父组件中的 dispatch 传递给子组件:

// 父组件
export const Father: React.FC = () => {// useReducer(fn, 初始数据, 对初始数据进行处理的fn)const [state, dispatch] = useReducer(reducer, defaultState, initAction)const onChangeName = () => {dispatch({ type: 'UPDATE_NAME', payload: '刘龙彬' })}return (<div><button onClick={onChangeName}>修改 name 的值</button><div className="father"><Son1 {...state} dispatch={dispatch} /><Son2 {...state} /></div></div>)
}

5、在 Son1 子组件中,扩充 React.FC<UserType> 的类型,并从 props 中把 dispatch用户信息对象分离出来:

// 子组件1
const Son1: React.FC<UserType & { dispatch: React.Dispatch<ActionType> }> = (props) => {const { dispatch, ...user } = propsconst add = () => dispatch({ type: 'INCREMENT', payload: 1 })return (<div className="son1"><p>用户信息:</p><p>{JSON.stringify(user)}</p><button onClick={add}>+1</button></div>)
}

8. 在子组件中实现点击按钮 age 自减操作

扩充 ActionType 的类型如下:

// 定义 action 的类型 
type ActionType = { type: 'UPDATE_NAME'; payload: string } | { type: 'INCREMENT'; payload: number } | { type: 'DECREMENT'; payload: number }

在 reducer 中添加 DECREMENT 的 case 匹配:

const reducer = (prevState: UserType, action: ActionType) => {console.log('触发了 reducer 函数', action)switch (action.type) {case 'UPDATE_NAME':return { ...prevState, name: action.payload }case 'INCREMENT':return { ...prevState, age: prevState.age + action.payload }// 添加 DECREMENT 的 case 匹配case 'DECREMENT':return { ...prevState, age: prevState.age - action.payload }default:return prevState}
}

在子组件 Son2 中添加 -5 的 button 按钮,并绑定点击事件处理函数:

// 子组件2
const Son2: React.FC<UserType> = (props) => {const sub = () => { }return (<div className="son2"><p>用户信息:</p><p>{JSON.stringify(props)}</p><button onClick={sub}>-5</button></div>)
}

现在的问题是:子组件 Son2 中无法调用到父组件的 dispatch 函数。为了解决这个问题,我们需要在 Father 父组件中,通过 props 把父组件中的 dispatch 传递给子组件:

// 父组件
export const Father: React.FC = () => {// useReducer(fn, 初始数据, 对初始数据进行处理的fn)const [state, dispatch] = useReducer(reducer, defaultState, initAction)const onChangeName = () => {dispatch({ type: 'UPDATE_NAME', payload: '刘龙彬' })}return (<div><button onClick={onChangeName}>修改 name 的值</button><div className="father"><Son1 {...state} dispatch={dispatch} /><Son2 {...state} dispatch={dispatch} /></div></div>)
}

在 Son2 子组件中,扩充 React.FC 的类型,并从 props 中把 dispatch用户信息对象分离出来:

// 子组件2
const Son2: React.FC<UserType & { dispatch: React.Dispatch<ActionType> }> = (props) => {const { dispatch, ...user } = propsconst sub = () => dispatch({ type: 'DECREMENT', payload: 5 })return (<div className="son2"><p>用户信息:</p><p>{JSON.stringify(user)}</p><button onClick={sub}>-5</button></div>)
}

9. 在 GrandSon 组件中实现重置按钮

1、扩充 ActionType 的类型如下:

// 定义 action 的类型 
type ActionType = { type: 'UPDATE_NAME'; payload: string } | { type: 'INCREMENT'; payload: number } | { type: 'DECREMENT'; payload: number } | { type: 'RESET' }

2、在 reducer 中添加 RESETcase 匹配:

const reducer = (prevState: UserType, action: ActionType) => {console.log('触发了 reducer 函数', action)switch (action.type) {case 'UPDATE_NAME':return { ...prevState, name: action.payload }case 'INCREMENT':return { ...prevState, age: prevState.age + action.payload }case 'DECREMENT':return { ...prevState, age: prevState.age - action.payload }// 添加 RESET 的 case 匹配case 'RESET':return defaultStatedefault:return prevState}
}

3、在 GrandSon 组件中,添加重置按钮,并绑定点击事件处理函数:

const GrandSon: React.FC<{ dispatch: React.Dispatch<ActionType> }> = (props) => {const reset = () => props.dispatch({ type: 'RESET' })return (<><h3>这是 GrandSon 组件</h3><button onClick={reset}>重置</button></>)
}

10. 使用 Immer 编写更简洁的 reducer 更新逻辑

解决每次重新为 对象/数组 解除引用的问题

1、安装 immer 相关的依赖包:

npm install immer use-immer -S

2、从 use-immer 中导入 useImmerReducer 函数,并替换掉 React 官方的 useReducer 函数的调用:

// 1. 导入 useImmerReducer
import { useImmerReducer } from 'use-immer'// 父组件
export const Father: React.FC = () => {// 2. 把 useReducer() 的调用替换成 useImmerReducer()const [state, dispatch] = useImmerReducer(reducer, defaultState, initAction)
}

3、修改 reducer 函数中的业务逻辑,case 代码块中不再需要 return 不可变的新对象了,只需要在 prevState 上进行修改即可。

Immer 内部会复制并返回新对象,因此降低了用户的心智负担。改造后的 reducer 代码如下:

const reducer = (prevState: UserType, action: ActionType) => {console.log('触发了 reducer 函数', action)switch (action.type) {case 'UPDATE_NAME':// return { ...prevState, name: action.payload }prevState.name = action.payloadbreakcase 'INCREMENT':// return { ...prevState, age: prevState.age + action.payload }prevState.age += action.payloadbreakcase 'DECREMENT':// return { ...prevState, age: prevState.age - action.payload }prevState.age -= action.payloadbreakcase 'RESET':return defaultStatedefault:return prevState}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/35552.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Zookeeper 集群的应用场景

Zookeeper 集群的应用场景 Zookeeper 是一个分布式协调服务,主要用于管理分布式应用中的配置、同步和命名等任务。由于其高可用性、 一致性和可靠性,Zookeeper 被广泛应用于各种分布式系统中。以下是 Zookeeper 集群的一些典型应用场景: 1. 配置管理 Zookeeper 可以用来集…

社区团购小程序开发

在快节奏的现代生活中&#xff0c;人们越来越追求便利与效率。社区团购小程序应运而生&#xff0c;以其独特的优势成为连接社区居民与优质商品的重要桥梁。本文将探讨社区团购小程序的特点、优势以及未来发展趋势&#xff0c;为大家揭示这一新型购物模式的魅力。 社区团购小程序…

LLM与GPT的一些概念

LLM 大模型语言模型(Large Language Model,LLM)技术是近年来人工智能领域的重要突破,凭借其出色的语义理解和生成能力,正在广泛应用于各种自然语言处理场景。 基本原理 LLM 是基于深度学习的语言模型,通过学习大规模文本数据,获得对自然语言的深入理解。这种模型能够准确地预…

MAC 查看公钥私钥

电脑配置过公钥私钥&#xff0c;现在需要查看&#xff1a; 1、 查看本地是否存在SSH密钥 命令&#xff1a;ls -al ~/.ssh 如果在输出的文件列表中发现id_rsa和id_rsa.pub的存在&#xff0c;证明本地已经存在SSH密钥&#xff0c;请执行第3步 2、 生成SSH密钥 命令&#xff1…

一本好的电子画册应这样做,你做对了吗?

​一本好的电子画册&#xff0c;不仅要有吸引人的图文&#xff0c;还可能包括视频、音频等多媒体元素&#xff0c;为读者提供全方位的阅读体验。连贯性是指画册的整体设计风格、内容布局要协调一致&#xff0c;让读者在阅读过程中感受到流畅和自然。创新性则要求创作者在内容呈…

39 - 电影评分(高频 SQL 50 题基础版)

39 - 电影评分 (selectu.name as results fromMovieRating m left join Users u on m.user_idu.user_id GROUP BYm.user_id order by count(*) desc,u.name asc limit 1) union all (selectm1.title as results fromMovieRating m left join Movies m1 on m.movie_idm1.movie…

加速业务布局,30年老将加盟ATFX,掌舵运营新篇章

全球领先的差价合约经纪商ATFX日前宣布了一项重大人事任命&#xff0c;聘请业界资深人士约翰博格(John Bogue)为机构业务运营总监。约翰博格是一名行业老将&#xff0c;曾在差价合约界深耕三十余载。伴随其加入ATFX&#xff0c;相信他的深厚专业知识和从业经验将为ATFX机构业务…

Java序列化进阶:Java内置序列化的三种方式

Java序列化就是把Java对象按照一定的格式存到文件或者磁盘当中 序列化的进阶&#xff1a;即三种方式&#xff0c;任何一种方式都可以进行序列化和反序列化 如果将数据读写到文档&#xff0c; 一般通过 ObjectOutputStream 将数据写入到文件当中&#xff0c;就是一种序列化的…

数据分析python基础实战分析

数据分析python基础实战分析 安装python&#xff0c;建议安装Anaconda 【Anaconda下载链接】https://repo.anaconda.com/archive/ 记得勾选上这个框框 安装完后&#xff0c;然后把这两个框框给取消掉再点完成 在电脑搜索框输入"Jupyter"&#xff0c;牛马启动&am…

简单聊聊云硬盘的规格

云硬盘类型及对应性能介绍 衡量云硬盘性能的指标有很多种&#xff0c;例如IOPS&#xff0c;吞吐量&#xff0c;读写时延&#xff1a; IOPS&#xff1a;云硬盘每秒进行读写的操作次数&#xff0c;可以细分到单盘最大IOPS&#xff0c;基线IOPS&#xff0c;IOPS突发上限等等。吞…

司美格鲁肽在中国获批!深度解析报告附上

在中国&#xff0c;肥胖问题日益严重&#xff0c;但有效的治疗方法却相对匮乏。然而&#xff0c;这一现状随着国家药品监督管理局&#xff08;NMPA&#xff09;对诺和诺德公司研发的司美格鲁肽注射液&#xff08;商品名&#xff1a;诺和盈&#xff09;的批准而得到改变。6月25日…

LabVIEW中卡尔曼滤波的作用与意义

卡尔曼滤波&#xff08;Kalman Filter&#xff09;是一种在控制系统和信号处理领域广泛应用的递推滤波算法&#xff0c;能够在噪声环境下对动态系统的状态进行最优估计。其广泛应用于导航、目标跟踪、图像处理、经济预测等多个领域。本文将详细介绍卡尔曼滤波在LabVIEW中的作用…

pytorch基础知识Tensor算术运算

1、Tensor的基本概念 标量是零维的张量&#xff0c;向量是一维的张量&#xff0c;矩阵是二维的张量 2、Tensor的创建 import torch"""常见的几个tensor创建""" a torch.Tensor([[1,2],[3,4]]) #2行2列的 print(a, a.type()) print(torch.on…

大数据平台需要存算分离吗?某保险集团:以 ZBS 优化资源利用率,缩短业务用时超一半

金融机构普遍采用“存算一体”架构支撑基于 Hadoop 框架的大数据平台。而随着金融业务的多元化发展&#xff0c;不同业务对计算和存储的需求差异较大&#xff0c;由于“存算一体”架构共享存储与计算资源&#xff0c;经常会出现资源需求不均衡、资源利用率低下、难以灵活调度等…

c++网络通信

TCP/IP协议 OSI参考模型采用分层划分原则&#xff0c;将网络中的数据传输划分为7层&#xff0c;其中&#xff0c;物理层居于最下层&#xff0c;是最基础、核心的网络硬件层&#xff1b;应用层居于最上层&#xff0c;负责应用资源的管理。每一层使用下层的服务&#xff0c;并向…

程序设计语言前言

1.机器语言及特点 2.编译语言及特点 3.高级语言及特点 4.编译和解释 5.IPO编程方式 一、机器语言 机器语言&#xff0c;也被称为二进制代码语言&#xff0c;是计算机硬件能够直接识别的程序语言或指令代码。它是由一系列由0和1组成的二进制指令码构成&#xff0c;每一条指令码…

【JavaScript脚本宇宙】轻松搞定代码调试和日志记录,你需要的都在这里!

掌握这几个JavaScript调试和日志库&#xff0c;让你的开发事半功倍&#xff01; 前言 在软件开发过程中&#xff0c;调试和日志记录是必不可少的环节。本文将介绍几个常用的JavaScript调试和日志记录库&#xff0c;包括debug、loglevel、Winston、Bunyan、Pino和Morgan&#…

drozer中文乱码解决方法

drozer简介 drozer 是 Android 的安全测试框架。 drozer 允许您通过扮演应用的角色并与 Android 运行时、其他应用的 IPC 端点和底层操作系统进行交互来搜索应用和设备中的安全漏洞。 drozer 提供了一些工具来帮助您使用、分享和理解公共 Android 漏洞。 drozer 是开源软件…

ESP32-C2模组数据透传模式配置详细教程

文章目录 1. 背景2. 关键步骤2.1 烧录AT指令固件2.2 配置透传模式2.3 如何退出透传模式重新配置3. 思考1. 背景 最近做的项目中,有蓝牙+WIFI的数据透传的需求,即系统A和系统B之间的通讯通过无线的方式,其实在实际项目中有很多这种场景比如无线调试手柄、无线数据终端、无线…

虚拟机热迁移详解:概念、架构、原理、搭建过程、常用命令与实战案例

一、虚拟机热迁移概述 1.1 虚拟机热迁移的定义 虚拟机热迁移&#xff08;Live Migration&#xff09;是指在不停止虚拟机运行的情况下&#xff0c;将其从一台物理主机迁移到另一台物理主机的过程。这一过程对用户和应用透明&#xff0c;几乎不会造成服务中断。热迁移技术在数…