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

相关文章

在Java中基于Geotools对PostGIS数据库的空间查询实践教程

《在Java中基于Geotools对PostGIS数据库的空间查询实践教程》本文将深入探讨这一实践,从连接配置到复杂空间查询操作,包括点查询、区域范围查询以及空间关系判断等,全方位展示如何在Java环... 目录前言一、相关技术背景介绍1、评价对象AOI2、数据处理流程二、对AOI空间范围查询实践1、空间查

qtcreater配置opencv遇到的坑及实践记录

《qtcreater配置opencv遇到的坑及实践记录》我配置opencv不管是按照网上的教程还是deepseek发现都有些问题,下面是我的配置方法以及实践成功的心得,感兴趣的朋友跟随小编一起看看吧... 目录电脑环境下载环境变量配置qmake加入外部库测试配置我配置opencv不管是按照网上的教程还是de

使用Python和Tkinter实现html标签去除工具

《使用Python和Tkinter实现html标签去除工具》本文介绍用Python和Tkinter开发的HTML标签去除工具,支持去除HTML标签、转义实体并输出纯文本,提供图形界面操作及复制功能,需... 目录html 标签去除工具功能介绍创作过程1. 技术选型2. 核心实现逻辑3. 用户体验增强如何运行

golang实现动态路由的项目实践

《golang实现动态路由的项目实践》本文主要介绍了golang实现动态路由项目实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习... 目录一、动态路由1.结构体(数据库的定义)2.预加载preload3.添加关联的方法一、动态路由1

CSS 样式表的四种应用方式及css注释的应用小结

《CSS样式表的四种应用方式及css注释的应用小结》:本文主要介绍了CSS样式表的四种应用方式及css注释的应用小结,本文通过实例代码给大家介绍的非常详细,详细内容请阅读本文,希望能对你有所帮助... 一、外部 css(推荐方式)定义:将 CSS 代码保存为独立的 .css 文件,通过 <link> 标签

使用Vue-ECharts实现数据可视化图表功能

《使用Vue-ECharts实现数据可视化图表功能》在前端开发中,经常会遇到需要展示数据可视化的需求,比如柱状图、折线图、饼图等,这类需求不仅要求我们准确地将数据呈现出来,还需要兼顾美观与交互体验,所... 目录前言为什么选择 vue-ECharts?1. 基于 ECharts,功能强大2. 更符合 Vue

Vue中插槽slot的使用示例详解

《Vue中插槽slot的使用示例详解》:本文主要介绍Vue中插槽slot的使用示例详解,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录一、插槽是什么二、插槽分类2.1 匿名插槽2.2 具名插槽2.3 作用域插槽三、插槽的基本使用3.1 匿名插槽

springboot+vue项目怎么解决跨域问题详解

《springboot+vue项目怎么解决跨域问题详解》:本文主要介绍springboot+vue项目怎么解决跨域问题的相关资料,包括前端代理、后端全局配置CORS、注解配置和Nginx反向代理,... 目录1. 前端代理(开发环境推荐)2. 后端全局配置 CORS(生产环境推荐)3. 后端注解配置(按接口

Vue 2 项目中配置 Tailwind CSS 和 Font Awesome 的最佳实践举例

《Vue2项目中配置TailwindCSS和FontAwesome的最佳实践举例》:本文主要介绍Vue2项目中配置TailwindCSS和FontAwesome的最... 目录vue 2 项目中配置 Tailwind css 和 Font Awesome 的最佳实践一、Tailwind CSS 配置1. 安

MyBatis分页插件PageHelper深度解析与实践指南

《MyBatis分页插件PageHelper深度解析与实践指南》在数据库操作中,分页查询是最常见的需求之一,传统的分页方式通常有两种内存分页和SQL分页,MyBatis作为优秀的ORM框架,本身并未提... 目录1. 为什么需要分页插件?2. PageHelper简介3. PageHelper集成与配置3.