ReactJS - 使用 useReducer



useReducer hook 是 useState hook 的高级版本。正如我们所知,useState 的目的是管理状态变量。useState 返回一个函数,该函数接受一个值并使用给定的值更新状态变量。


// counter = 0
const [counter, setCounter] = useState(0)

// counter = 1
setCounter(1)

// counter = 2
setCounter(2)

useReducer 钩子接受一个 reducer 函数和初始值,并返回一个调度器函数。Reducer 函数将接受初始状态和动作(特定场景),然后根据动作提供更新状态的逻辑。调度器函数接受操作(以及相应的详细信息),并使用提供的操作调用 reducer 函数。

例如,useReducer 可用于根据递增和递减操作更新计数器状态。递增操作会将计数器状态递增 1,递减操作会将计数器递减 1。

在本章中,让我们学习如何使用 useReducer 钩子。

useReducer 钩子的签名

useReducer 钩子的签名如下 -


 const [<state>, <dispatch function>] = useReducer(<reducer function>, <initial argument>, <init function>);

这里

  • state 表示要在状态中维护的信息
  • Reducer 函数是一个 JavaScript 函数,用于根据动作更新状态。

以下是 reducer 函数的语法 -


 (<state>, <action>) => <updated state>

哪里

  • state − 当前状态信息
  • action - 要执行的动作(应有有效载荷来执行该动作)
  • updated state − 更新状态
  • initial argument − 表示状态的初始值
  • init function − 表示初始化函数,可用于设置状态的初始值/重置当前值。如果需要计算初始值,那么我们可以使用 init 函数。否则,可以跳过该参数。

应用减速钩

让我们创建一个 react 应用程序来管理一系列待办事项。首先,我们将使用 useState 实现它,然后将其转换为使用 useReducer。通过使用两个钩子实现应用程序,我们将理解 useReducer 相对于 useState 的好处。此外,我们可以根据情况明智地选择钩子。

首先,创建一个新的 react 应用程序并使用以下命令启动它。

create-react-app myapp
cd myapp
npm start

接下来,在组件文件夹 (src/components/TodoList.js) 下创建一个 react 组件 TodoList。


function TodoList() {
	 	return <div>Todo List</div>
}
export default TodoList

接下来,更新根组件,App.js使用新创建的 TodoList 组件。


import logo from './logo.svg';
import './App.css';
import TodoList from './components/TodoList';
function App() {
	 	return (
	 	 	 <div style={{ padding: "5px"}}>
	 	 	 	 	<TodoList />
	 	 	 </div>
	 	);
}
export default App;

接下来,创建一个变量 todoData 来管理待办事项列表,并使用 useState 钩子将其设置为状态。


const [todoData, setTodoData] = useState({
	 	action: '',
	 	items: [],
	 	newItem: null,
	 	id: 0
})

这里

  • action 用于表示当前 action, add & delete 要应用于当前待办事项(项)列表。
  • items 是一个数组,用于保存待办事项的当前列表。
  • newItem 是用于表示当前待办事项的对象。该对象将有两个字段,id 和 todo。
  • id 是在删除操作期间要删除的当前项目的 ID。
  • useState 用于获取和设置待办事项列表 (todoData)。

接下来,呈现待办事项的当前列表 (todoData.items) 以及删除按钮和用于输入新待办事项的输入文本字段。


<div>
	 	<p>List of Todo list</p>
	 	<ul>
	 	 	 {todoData.items && todoData.items.map((item) =>
	 	 	 	 	<li key={item.id}>{item.todo} <span><button onClick={(e) =>
	 	 	 	 	handleDeleteButton(item.id, e)}>Delete</button></span></li>
	 	 	 )}
	 	 	 <li><input type="text" name="todo" onChange={handleInput} />
	 	 	 <button onClick={handleAddButton}>Add</button></li>
	 	</ul>
</div>

这里

  • 呈现来自状态变量 todoData 的当前待办事项列表。
  • 呈现了一个输入字段,供用户输入新的待办事项,并附加了 onChange 事件处理程序 handleInput。事件处理程序将使用用户在输入字段中输入的数据更新处于待办事项状态 (todoData) 的 newItem。
  • 为用户呈现了一个按钮,用于将新输入的待办事项添加到当前待办事项列表中,并附加了 onClick 事件处理程序 handleAddButton。事件处理程序会将当前/新的待办事项添加到待办事项状态。
  • 为待办事项列表中的每个项目呈现一个按钮,并附加了 onClick 事件处理程序 handleDeleteButton。事件处理程序将从待办事项状态中删除相应的待办事项。

接下来,实现handleInput事件处理程序,如下所示 -


const handleInput = (e) => {
	 	var id = 0
	 	if(todoData.newItem == null) {
	 	 	 for(let i = 0; i < todoData.items.length; i++) {
	 	 	 	 	if(id < todoData.items[i].id) {
	 	 	 	 	 	 id = todoData.items[i].id
	 	 	 	 	}
	 	 	 }
	 	 	 id += 1
	 	} else {
	 	 	 id = todoData.newItem.id
	 	}
	 	let data = {
	 	 	 actions: '',
	 	 	 items: todoData.items,
	 	 	 newItem: {
	 	 	 	 	id: id,
	 	 	 	 	todo: e.target.value
	 	 	 },
	 	 	 id: 0
	 	}
	 	setTodoData(data)
}

在这里,我们有,

  • 使用用户输入的数据 (e.target.value) 更新了 newItem.todo)
  • 已创建并设置新项目的 ID。

接下来,实现handleDeleteButton事件处理程序,如下所示 -


const handleDeleteButton = (deleteId, e) => {
	 	let data = {
	 	 	 action: 'delete',
	 	 	 items: todoData.items,
	 	 	 newItem: todoData.newItem,
	 	 	 id: deleteId
	 	}
	 	setTodoData(data)
}

在这里,处理程序设置要删除的待办事项的 id (deleteid) 和处于待办事项状态的删除操作

接下来,实现handleAddButton事件处理程序,如下所示 -


const handleAddButton = () => {
	 	let data = {
	 	 	 action: 'add',
	 	 	 items: todoData.items,
	 	 	 newItem: todoData.newItem,
	 	 	 id: 0
	 	}
	 	setTodoData(data)
}

在这里,处理程序设置新项并在状态中添加操作

接下来,实现如下所示的添加操作 -


if(todoData.action == 'add') {
	 	if(todoData.newItem != null) {
	 	 	 let data = {
	 	 	 	 	action: '',
	 	 	 	 	items: [...todoData.items, todoData.newItem],
	 	 	 	 	newItem: null,
	 	 	 	 	id: 0
	 	 	 }
	 	 	 setTodoData(data)
	 	}
}

在这里,新项以 todo 状态添加到现有列表 (todoData.items) 中。

接下来,实现删除操作,如下所示 -


if(todoData.action == 'delete' && todoData.id != 0) {
	 	var newItemList = []
	 	for(let i = 0; i < todoData.items.length; i++) {
	 	 	 if(todoData.items[i].id != todoData.id) {
	 	 	 	 	newItemList.push(todoData.items[i])
	 	 	 }
	 	}
	 	let data = {
	 	 	 action: '',
	 	 	 items: newItemList,
	 	 	 newItem: null,
	 	 	 id: 0
	 	}
	 	setTodoData(data)
}

在这里,具有指定 id 的项目将从待办事项列表 (todoData.items) 中删除。

该组件的完整源代码如下:


import { useState } from "react"
function TodoList() {
	 	const [todoData, setTodoData] = useState({
	 	 	 action: '',
	 	 	 items: [],
	 	 	 newItem: null,
	 	 	 id: 0
	 	})
	 	if(todoData.action == 'add') {
	 	 	 if(todoData.newItem != null) {
	 	 	 	 	let data = {
	 	 	 	 	 	 action: '',
	 	 	 	 	 	 items: [...todoData.items, todoData.newItem],
	 	 	 	 	 	 newItem: null,
	 	 	 	 	 	 id: 0
	 	 	 	 	}
	 	 	 	 	setTodoData(data)
	 	 	 }
	 	}
	 	if(todoData.action == 'delete' && todoData.id != 0) {
	 	 	 var newItemList = []
	 	 	 for(let i = 0; i < todoData.items.length; i++) {
	 	 	 	 	if(todoData.items[i].id != todoData.id) {
	 	 	 	 	 	 newItemList.push(todoData.items[i])
	 	 	 	 	}
	 	 	 }
	 	 	 let data = {
	 	 	 	 	action: '',
	 	 	 	 	items: newItemList,
	 	 	 	 	newItem: null,
	 	 	 	 	id: 0
	 	 	 }
	 	 	 setTodoData(data)
	 	}
	 	const handleInput = (e) => {
	 	 	 var id = 0
	 	 	 if(todoData.newItem == null) {
	 	 	 	 	for(let i = 0; i < todoData.items.length; i++) {
	 	 	 	 	 	 if(id < todoData.items[i].id) {
	 	 	 	 	 	 	 	id = todoData.items[i].id
	 	 	 	 	 	 }
	 	 	 	 	}
	 	 	 	 	id += 1
	 	 	 } else {
	 	 	 	 	id = todoData.newItem.id
	 	 	 }
	 	 	 let data = {
	 	 	 	 	action: '',
	 	 	 	 	items: todoData.items,
	 	 	 	 	newItem: {
	 	 	 	 	 	 id: id,
	 	 	 	 	 	 todo: e.target.value
	 	 	 	 	},
	 	 	 	 	id: 0
	 	 	 }
	 	 	 setTodoData(data)
	 	}
	 	const handleDeleteButton = (deleteId, e) => {
	 	 	 let data = {
	 	 	 	 	action: 'delete',
	 	 	 	 	items: todoData.items,
	 	 	 	 	newItem: todoData.newItem,
	 	 	 	 	id: deleteId
	 	 	 }
	 	 	 setTodoData(data)
	 	}
	 	const handleAddButton = () => {
	 	 	 let data = {
	 	 	 	 	action: 'add',
	 	 	 	 	items: todoData.items,
	 	 	 	 	newItem: todoData.newItem,
	 	 	 	 	id: 0
	 	 	 }
	 	 	 setTodoData(data)
	 	}
	 	return (
	 	 	 <div>
	 	 	 	 	<p>List of Todo list</p>
	 	 	 	 	<ul>
	 	 	 	 	 	 {todoData.items && todoData.items.map((item) =>
	 	 	 	 	 	 <li key={item.id}>{item.todo} <span><button onClick={(e) => handleDeleteButton(item.id, e)}>Delete</button></span></li>
	 	 	 	 	 	 )}
	 	 	 	 	 	 <li><input type="text" name="todo" onChange={handleInput} /><button onClick={handleAddButton}>Add</button></li>
	 	 	 	 	</ul>
	 	 	 </div>
	 	)
}
export default TodoList

在这里,应用程序使用 useState 钩子来执行功能。

接下来,打开浏览器并添加/删除待办事项。该应用程序将如下所示 -

Applying Reducer Hook

使用 useReducer

让我们使用 useReducer 重新实现功能

首先,在组件文件夹(src/components/TodoReducerList.js)下创建一个 react 组件 TodoReducerList


function TodoReducerList() {
	 	return <div>Todo List</div>
}
export default TodoReducerList

接下来,更新根组件,App.js使用新创建的 TodoReducerList 组件。


import './App.css';
import TodoReducerList from './components/TodoReducerList';
function App() {
	 	return (
	 	 	 <div style={{ padding: "5px"}}>
	 	 	 	 	<TodoReducerList />
	 	 	 </div>
	 	);
}
export default App;

接下来,实现一个 reducer 函数 todoReducer,它将接收两个参数,如下所示 -

  • 当前待办事项列表(项目)
  • 操作 (action.type) 以及与操作相关的信息 (action.payload)。对于添加操作 (action.type),有效负载 (action.payload) 将具有新的待办事项,对于删除操作 (action.type),它将具有要删除的待办事项的 ID

Reducer 会将相关操作应用于待办事项列表,并发回修改后的待办事项列表,如下所示 -


function todoReducer(items, action) {
	 	// action = { type: 'add / delete', payload: 'new todo item'}
	 	let newTodoList = []
	 	
	 	switch (action.type) {
	 	 	 case 'add':
	 	 	 	 	var id = 0
	 	 	 	 	for(let i = 0; i < items.length; i++) {
	 	 	 	 	 	 if(id < items[i].id) {
	 	 	 	 	 	 	 	id = items[i].id
	 	 	 	 	 	 }
	 	 	 	 	}
	 	 	 	 	action.payload.id = id + 1
	 	 	 	 	newTodoList = [...items, action.payload]
	 	 	 break;
	 	 	 case 'delete':
	 	 	 	 	for(let i = 0; i < items.length; i++) {
	 	 	 	 	 	 if(items[i].id != action.payload.id) {
	 	 	 	 	 	 	 	newTodoList.push(items[i])
	 	 	 	 	 	 }
	 	 	 	 	}
	 	 	 break;
	 	 	 default:
	 	 	 	 	throw new Error()
	 	}
	 	return newTodoList
}

在这里,我们有,

  • 使用了 switch 语句两个句柄动作。
  • 添加的操作将有效负载添加到现有的待办事项列表。
  • 已删除操作将从现有待办事项列表中删除有效负载中指定的项
  • 最后,该函数返回更新的待办事项列表

接下来,在 TodoReducerList 组件中使用新创建的 reducer,如下所示 -


 const [items, dispatch] = useReducer(todoReducer, [])

这里,useReducer 接收两个参数,a) reducer 函数和 b) 当前待办事项列表,并返回当前待办事项列表和调度器函数调度。调度程序函数 (dispatch) 应使用具有相关有效负载信息的添加或删除操作来调用。

接下来,为输入文本字段(新的待办事项)和两个按钮(添加和删除)创建处理函数,如下所示 -


const [todo, setTodo] = useState('')
const handleInput = (e) => {
	 	setTodo(e.target.value)
}
const handleAddButton = () => {
	 	dispatch({
	 	 	 type: 'add',
	 	 	 payload: {
	 	 	 	 	todo: todo
	 	 	 }
	 	})
	 	setTodo('')
}
const handleDeleteButton = (id) => {
	 	dispatch({
	 	 	 type: 'delete',
	 	 	 payload: {
	 	 	 id: id
	 	 	 }
	 	})
}

在这里,我们有,

  • 使用 useState 管理用户输入 (todo)
  • 使用 dispatch 方法处理相关处理程序中的添加和删除操作
  • 在处理程序中传递特定于操作的有效负载。

接下来,更新渲染方法,如下所示 -


<div>
	 	<p>List of Todo list</p>
	 	<ul>
	 	 	 {items && items.map((item) =>
	 	 	 	 	<li key={item.id}>{item.todo} <span><button onClick={(e) =>
	 	 	 	 	handleDeleteButton(item.id)}>Delete</button></span></li>
	 	 	 	)}
	 	 	 <li><input type="text" name="todo" value={todo} onChange={handleInput} />
	 	 	 <button onClick={handleAddButton}>Add</button></li>
	 	</ul>
</div>

该组件的完整源代码以及 reducer 函数如下 -


import {
	 	useReducer,
	 	useState
} from "react"
function todoReducer(items, action) {
	 	
	 	// action = { type: 'add / delete', payload: 'new todo item'}
	 	let newTodoList = []
	 	switch (action.type) {
	 	 	 case 'add':
	 	 	 	 	var id = 0
	 	 	 	 	for(let i = 0; i < items.length; i++) {
	 	 	 	 	 	 if(id < items[i].id) {
	 	 	 	 	 	 	 	id = items[i].id
	 	 	 	 	 	 }
	 	 	 	 	}
	 	 	 	 	action.payload.id = id + 1
	 	 	 	 	newTodoList = [...items, action.payload]
	 	 	 break;
	 	 	 case 'delete':
	 	 	 	 	for(let i = 0; i < items.length; i++) {
	 	 	 	 	 	 if(items[i].id != action.payload.id) {
	 	 	 	 	 	 	 	newTodoList.push(items[i])
	 	 	 	 	}
	 	 	 }
	 	 	 break;
	 	 	 default:
	 	 	 	 	throw new Error()
	 	}
	 	return newTodoList
}
function TodoReducerList() {
	 	const [todo, setTodo] = useState('')
	 	const [items, dispatch] = useReducer(todoReducer, [])
	 	const handleInput = (e) => {
	 	 	 setTodo(e.target.value)
	 	}
	 	const handleAddButton = () => {
	 	 	 dispatch({
	 	 	 	 	type: 'add',
	 	 	 	 	payload: {
	 	 	 	 	 	 todo: todo
	 	 	 	 	}
	 	 	 })
	 	 	 setTodo('')
	 	}
	 	const handleDeleteButton = (id) => {
	 	 	 dispatch({
	 	 	 	 	type: 'delete',
	 	 	 	 	payload: {
	 	 	 	 	 	 id: id
	 	 	 	 	}
	 	 	 })
	 	}
	 	return (
	 	 	 <div>
	 	 	 	 	<p>List of Todo list</p>
	 	 	 	 	<ul>
	 	 	 	 	 	 {items && items.map((item) =>
	 	 	 	 	 	 <li key={item.id}>{item.todo} <span><button onClick={(e) =>	
	 	 	 	 	 	 	 	handleDeleteButton(item.id)}>Delete</button></span></li>
	 	 	 	 	 	 )}
	 	 	 	 	 	 <li><input type="text" name="todo" value={todo} onChange={handleInput} />
	 	 	 	 	 	 	 	<button onClick={handleAddButton}>Add</button></li>
	 	 	 	 	</ul>
	 	 	 </div>
	 	)
}
export default TodoReducerList

接下来,打开浏览器并检查输出。

Using UseReducer

我们可以清楚地了解到,与纯粹的 useState 实现相比,useReducer 实现简单、易行、易懂。

总结

useReducer 钩子在状态管理中引入了 reducer 模式。它鼓励代码重用,并提高组件的可读性和可理解性。总的来说,useReducer 是 react 开发者工具包中必不可少且强大的工具。