react useReducer hook实践

2023-10-23 19:12

本文主要是介绍react useReducer hook实践,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

您好, 如果喜欢我的文章或者想上岸大厂,可以关注公众号「量子前端」,将不定期关注推送前端好文、分享就业资料秘籍,也希望有机会一对一帮助你实现梦想

前言

随着应用逐渐复杂,我们经常发现useState在管理复杂的状态逻辑时显得有些力不从心。这时,React为我们提供的另一个更为强大的hook——useReducer——可以帮助我们优雅地处理复杂状态。

useReducer允许我们使用 action 和 reducer 的方式来组织复杂的状态逻辑,使其变得更加清晰和模块化,弥补了useState的局限性。

基础用法

useState相似,useReducer也是 React 的 Hook,而且也只能放在组件最顶层使用。与前者不同的地方在于,它是通过 action 来更新状态的,使状态更新逻辑更具可读性。

useReducer接收三个参数

  • reducer 函数:指定如何更新状态的还原函数,它必须是纯函数,以 state 和 dispatch 为参数,并返回下一个状态。
  • 初始状态:初始状态的计算值。
  • (可选的)初始化参数:用于返回初始状态。如果未指定,初始状态将设置为 initialArg;如果有指定,初始状态将被设置为调用init(initialArg)的结果。

useReducer返回两个参数

  • 当前的状态:当前状态。在第一次渲染时,它会被设置为init(initialArg)或 initialArg(如果没有 init 的情况下)。
  • dispatch:调度函数,用于调用 reducer 函数,以更新状态并触发重新渲染。

基本形式如下:

jsx
复制代码
const [state, dispatch] = useReducer(reducer, initialArg, init?)

通常情况下,我们只会用到useReducer的前两个参数,如这个计数器组件:

jsx
复制代码
const initialState = { count: 0 };function reducer(state, action) {switch (action.type) {case 'increment':return { count: state.count + 1 };case 'decrement':return { count: state.count - 1 };default:throw new Error();}
}function Counter() {const [state, dispatch] = useReducer(reducer, initialState);return (<>Count: {state.count}<button onClick={() => dispatch({ type: 'decrement' })}>-</button><button onClick={() => dispatch({ type: 'increment' })}>+</button></>);
}

使用dispatch的注意事项

  • dispatch调用后,状态更新是异步的,因此立刻读取状态可能仍是旧的。

    jsx
    复制代码
    function addCount() {dispatch({ type: 'increment' })console.log(state.count) // 打印出来的不是新值
    }<button onClick={addCount}>+</button>
    
  • React 对dispatch有一个优化机制:如果dispatch触发更新前后的值相等(使用Object.is判断),实际上 React 不会进行重新渲染,这是出于性能考虑。

使用reducer函数的注意事项

你在reducer里面更新对象和数组的状态,需要创建一个新的对象或数组,而不是在原对象和数组上修改,这一点和useState是一样的。

初始化状态:使用init函数

上一节我们提到了useReducer还有第三个参数init,那么它的作用是什么?它也是为了性能优化而来。

我们先假设一个场景,计数器的值保存在localStorage里面,进入页面的时候,我们希望从localStorage中读取值来作为useReducer初值,如果没有init,我们可以这样做:

jsx
复制代码
function getInitialCount() {const savedCount = localStorage.getItem("count");return savedCount ? Number(savedCount) : 0;
}function counterReducer(state, action) {switch (action.type) {case "INCREMENT":return { count: state.count + 1 };case "DECREMENT":return { count: state.count - 1 };default:return state;}
}function Counter() {const [state, dispatch] = useReducer(counterReducer, getInitialCount());// 使用useEffect来监听状态的变化,并将其保存到localStorageuseEffect(() => {localStorage.setItem("count", state.count);}, [state.count]);return (<>Count: {state.count}<button onClick={() => dispatch({ type: "INCREMENT" })}>+1</button><button onClick={() => dispatch({ type: "DECREMENT" })}>-1</button></>);
}

在这个例子中,我们直接调用getInitialCount函数作为useReducer的第二个参数,从而得到初始状态。当React初始化这个组件时,它会执行这个函数并使用其返回值作为初始状态。

如果在第三个参数里进行初始化,代码是这样写:

jsx
复制代码
function init(initialValue) {// 尝试从localStorage中读取值const savedCount = localStorage.getItem("count");// 如果有值并且可以被解析为数字,则返回它,否则返回initialValuereturn { count: savedCount ? Number(savedCount) : initialValue };
}function counterReducer(state, action) {switch (action.type) {case "INCREMENT":return { count: state.count + 1 };case "DECREMENT":return { count: state.count - 1 };default:return state;}
}function Counter() {const [state, dispatch] = useReducer(counterReducer, 0, init);// 使用useEffect来监听状态的变化,并将其保存到localStorageuseEffect(() => {localStorage.setItem("count", state.count);}, [state.count]);return (<>Count: {state.count}<button onClick={() => dispatch({ type: "INCREMENT" })}>+1</button><button onClick={() => dispatch({ type: "DECREMENT" })}>-1</button></>);
}

这两种方式看似差不多,但它们区别很大:

  1. 执行时机

    • 直接调用函数作为第二个参数:这个函数会在每次组件渲染时执行。
    • 使用init函数:init函数只在组件初次渲染时执行一次。
  2. 访问到的数据

    • 直接调用函数作为第二个参数:这个函数只能访问到定义它时的作用域内的数据。
    • 使用init函数:由于init函数接受initialArg作为参数,这使得init函数具有更大的灵活性,能够基于传入的参数进行计算。
  3. 代码组织

    • 直接调用函数作为第二个参数:这通常更简洁,适合那些简单的初始化逻辑。
    • 使用init函数:它提供了更清晰的代码组织结构,特别是当初始化逻辑相对复杂或需要根据传入的参数变化时。
  4. 性能

    • 直接调用函数作为第二个参数:如果这个函数执行了一些计算密集或副作用的操作,那么在每次组件渲染时都会执行,可能会导致性能问题。
    • 使用init函数:由于它只在组件的初始化阶段执行一次,所以对于那些计算密集的初始化操作,使用init函数可能会更为高效。

总结一下,两者都可以用于初始化状态,如果你的初始化逻辑简单并且没有性能顾虑,可以直接使用一个函数作为useReducer的第二个参数,但如果你需要基于传入的参数来决定初始化逻辑或者想确保性能最优的做法,那么应该使用init函数。

高级技巧

中间件

就像Redux中的中间件,我们可以利用dispatch创建一个中间件方法,支持调用dispatch之前或之后执行代码。

jsx
复制代码
function thunkMiddleware(dispatch) {return function(action) {if (typeof action === 'function') {action(dispatch);} else {dispatch(action);}// 代码在dispatch之后执行console.log("Action dispatched at: ", new Date().toISOString());};
}function fetchData() {return dispatch => {fetch("/api/data").then(res => res.json()).then(data => dispatch({ type: 'SET_DATA', payload: data }));};
}function App() {const [state, unenhancedDispatch] = useReducer(reducer, initialState);const dispatch = thunkMiddleware(unenhancedDispatch);useEffect(() => {dispatch(fetchData());}, [dispatch]);
}

在这个示例中,通过将原始的dispatch包裹在另一个函数内部,中间件为我们提供了一个在真正的状态更新前后注入自定义逻辑的机会。

示例中,我们在调用原始的dispatch之前首先检查了action的类型。实际上,你可以在这里添加任何你想要的逻辑,例如日志记录、错误处理、请求API等。在dispatch调用之后,依然可以添加额外的逻辑。

useContext一起使用

结合useContextuseReducer可以创建简单的全局状态管理系统。

我们就以此来尝试创建一个完整的主题切换系统:

首先,定义状态、reducer 和 context:

jsx
复制代码
const ThemeContext = React.createContext();const initialState = { theme: 'light' };function themeReducer(state, action) {switch (action.type) {case 'TOGGLE_THEME':return { theme: state.theme === 'light' ? 'dark' : 'light' };default:return state;}
}

接下来,创建一个Provider组件:

jsx
复制代码
function ThemeProvider({ children }) {const [state, dispatch] = useReducer(themeReducer, initialState);return (<ThemeContext.Provider value={{ theme: state.theme, toggleTheme: () => dispatch({ type: 'TOGGLE_THEME' }) }}>{children}</ThemeContext.Provider>);
}

在子组件中,你可以轻松切换和读取主题:

jsx
复制代码
function ThemedButton() {const { theme, toggleTheme } = useContext(ThemeContext);return (<button style={{ backgroundColor: theme === 'light' ? '#fff' : '#000' }} onClick={toggleTheme}>Toggle Theme</button>);
}

useReducer与 Redux 的差异

虽然useReducer和 Redux 都采用了 action 和 reducer 的模式来处理状态,但它们在实现和使用上有几个主要的区别:

  • 范围useReducer通常在组件或小型应用中使用,而Redux被设计为大型应用的全局状态管理工具。
  • 中间件和扩展:Redux支持中间件,这允许开发者插入自定义逻辑,例如日志、异步操作等。而useReducer本身不直接支持,但我们可以模拟中间件的效果。
  • 复杂性:对于简单的状态管理,useReducer通常更简单和直接。但当涉及到复杂的状态逻辑和中间件时,Redux可能更具优势。

结语

useReducer作为 React 的一部分,它比useState强大,又比 Redux 轻量,尤其适合中小型应用或组件级状态管理。本文把useReducer的用法和注意项完整的讲解了一遍,吃透其中的知识点就能保证你对useReducer有足够的了解了。

如果喜欢我的文章或者想上岸大厂,可以关注公众号「量子前端」,将不定期关注推送前端好文、分享就业资料秘籍,也希望有机会一对一帮助你实现梦想。

这篇关于react useReducer hook实践的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/269907

相关文章

全面掌握 SQL 中的 DATEDIFF函数及用法最佳实践

《全面掌握SQL中的DATEDIFF函数及用法最佳实践》本文解析DATEDIFF在不同数据库中的差异,强调其边界计算原理,探讨应用场景及陷阱,推荐根据需求选择TIMESTAMPDIFF或inte... 目录1. 核心概念:DATEDIFF 究竟在计算什么?2. 主流数据库中的 DATEDIFF 实现2.1

Spring WebFlux 与 WebClient 使用指南及最佳实践

《SpringWebFlux与WebClient使用指南及最佳实践》WebClient是SpringWebFlux模块提供的非阻塞、响应式HTTP客户端,基于ProjectReactor实现,... 目录Spring WebFlux 与 WebClient 使用指南1. WebClient 概述2. 核心依

MyBatis-Plus 中 nested() 与 and() 方法详解(最佳实践场景)

《MyBatis-Plus中nested()与and()方法详解(最佳实践场景)》在MyBatis-Plus的条件构造器中,nested()和and()都是用于构建复杂查询条件的关键方法,但... 目录MyBATis-Plus 中nested()与and()方法详解一、核心区别对比二、方法详解1.and()

Spring Boot @RestControllerAdvice全局异常处理最佳实践

《SpringBoot@RestControllerAdvice全局异常处理最佳实践》本文详解SpringBoot中通过@RestControllerAdvice实现全局异常处理,强调代码复用、统... 目录前言一、为什么要使用全局异常处理?二、核心注解解析1. @RestControllerAdvice2

Spring事务传播机制最佳实践

《Spring事务传播机制最佳实践》Spring的事务传播机制为我们提供了优雅的解决方案,本文将带您深入理解这一机制,掌握不同场景下的最佳实践,感兴趣的朋友一起看看吧... 目录1. 什么是事务传播行为2. Spring支持的七种事务传播行为2.1 REQUIRED(默认)2.2 SUPPORTS2

Java中的雪花算法Snowflake解析与实践技巧

《Java中的雪花算法Snowflake解析与实践技巧》本文解析了雪花算法的原理、Java实现及生产实践,涵盖ID结构、位运算技巧、时钟回拨处理、WorkerId分配等关键点,并探讨了百度UidGen... 目录一、雪花算法核心原理1.1 算法起源1.2 ID结构详解1.3 核心特性二、Java实现解析2.

MySQL 中 ROW_NUMBER() 函数最佳实践

《MySQL中ROW_NUMBER()函数最佳实践》MySQL中ROW_NUMBER()函数,作为窗口函数为每行分配唯一连续序号,区别于RANK()和DENSE_RANK(),特别适合分页、去重... 目录mysql 中 ROW_NUMBER() 函数详解一、基础语法二、核心特点三、典型应用场景1. 数据分

深度解析Spring AOP @Aspect 原理、实战与最佳实践教程

《深度解析SpringAOP@Aspect原理、实战与最佳实践教程》文章系统讲解了SpringAOP核心概念、实现方式及原理,涵盖横切关注点分离、代理机制(JDK/CGLIB)、切入点类型、性能... 目录1. @ASPect 核心概念1.1 AOP 编程范式1.2 @Aspect 关键特性2. 完整代码实

前端如何通过nginx访问本地端口

《前端如何通过nginx访问本地端口》:本文主要介绍前端如何通过nginx访问本地端口的问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、nginx安装1、下载(1)下载地址(2)系统选择(3)版本选择2、安装部署(1)解压(2)配置文件修改(3)启动(4)

HTML中meta标签的常见使用案例(示例详解)

《HTML中meta标签的常见使用案例(示例详解)》HTMLmeta标签用于提供文档元数据,涵盖字符编码、SEO优化、社交媒体集成、移动设备适配、浏览器控制及安全隐私设置,优化页面显示与搜索引擎索引... 目录html中meta标签的常见使用案例一、基础功能二、搜索引擎优化(seo)三、社交媒体集成四、移动