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

相关文章

Spring Security简介、使用与最佳实践

《SpringSecurity简介、使用与最佳实践》SpringSecurity是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架,本文给大家介绍SpringSec... 目录一、如何理解 Spring Security?—— 核心思想二、如何在 Java 项目中使用?——

防止Linux rm命令误操作的多场景防护方案与实践

《防止Linuxrm命令误操作的多场景防护方案与实践》在Linux系统中,rm命令是删除文件和目录的高效工具,但一旦误操作,如执行rm-rf/或rm-rf/*,极易导致系统数据灾难,本文针对不同场景... 目录引言理解 rm 命令及误操作风险rm 命令基础常见误操作案例防护方案使用 rm编程 别名及安全删除

Vue和React受控组件的区别小结

《Vue和React受控组件的区别小结》本文主要介绍了Vue和React受控组件的区别小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录背景React 的实现vue3 的实现写法一:直接修改事件参数写法二:通过ref引用 DOMVu

Java实现将HTML文件与字符串转换为图片

《Java实现将HTML文件与字符串转换为图片》在Java开发中,我们经常会遇到将HTML内容转换为图片的需求,本文小编就来和大家详细讲讲如何使用FreeSpire.DocforJava库来实现这一功... 目录前言核心实现:html 转图片完整代码场景 1:转换本地 HTML 文件为图片场景 2:转换 H

C++统计函数执行时间的最佳实践

《C++统计函数执行时间的最佳实践》在软件开发过程中,性能分析是优化程序的重要环节,了解函数的执行时间分布对于识别性能瓶颈至关重要,本文将分享一个C++函数执行时间统计工具,希望对大家有所帮助... 目录前言工具特性核心设计1. 数据结构设计2. 单例模式管理器3. RAII自动计时使用方法基本用法高级用法

C#使用Spire.Doc for .NET实现HTML转Word的高效方案

《C#使用Spire.Docfor.NET实现HTML转Word的高效方案》在Web开发中,HTML内容的生成与处理是高频需求,然而,当用户需要将HTML页面或动态生成的HTML字符串转换为Wor... 目录引言一、html转Word的典型场景与挑战二、用 Spire.Doc 实现 HTML 转 Word1

PHP应用中处理限流和API节流的最佳实践

《PHP应用中处理限流和API节流的最佳实践》限流和API节流对于确保Web应用程序的可靠性、安全性和可扩展性至关重要,本文将详细介绍PHP应用中处理限流和API节流的最佳实践,下面就来和小编一起学习... 目录限流的重要性在 php 中实施限流的最佳实践使用集中式存储进行状态管理(如 Redis)采用滑动

Vue3绑定props默认值问题

《Vue3绑定props默认值问题》使用Vue3的defineProps配合TypeScript的interface定义props类型,并通过withDefaults设置默认值,使组件能安全访问传入的... 目录前言步骤步骤1:使用 defineProps 定义 Props步骤2:设置默认值总结前言使用T

ShardingProxy读写分离之原理、配置与实践过程

《ShardingProxy读写分离之原理、配置与实践过程》ShardingProxy是ApacheShardingSphere的数据库中间件,通过三层架构实现读写分离,解决高并发场景下数据库性能瓶... 目录一、ShardingProxy技术定位与读写分离核心价值1.1 技术定位1.2 读写分离核心价值二

深入浅出Spring中的@Autowired自动注入的工作原理及实践应用

《深入浅出Spring中的@Autowired自动注入的工作原理及实践应用》在Spring框架的学习旅程中,@Autowired无疑是一个高频出现却又让初学者头疼的注解,它看似简单,却蕴含着Sprin... 目录深入浅出Spring中的@Autowired:自动注入的奥秘什么是依赖注入?@Autowired