React Hooks - useReducer
useReducer
useState
当您具有涉及多个子值的复杂状态逻辑或下一个状态依赖于前一个状态时,通常比更可取。
初始化
类似于useState
,调用时会返回一个包含两个项目的数组。第一个项目是当前状态,第二个项目是调度方法。我们使用数组解构useReducer
将这两个返回值赋值给变量。
const [state, dispatch] = useReducer(reducer, initialState);
useReducer
接受两个参数和(以及一个可选的第三个参数,我们稍后会介绍)。第一个参数是一个 Reducer 函数,第二个参数是我们的初始状态值,类似于useState
。
什么是 Reducer?
Reducer 函数并非 React 独有。它们只是一些 JavaScript 函数,接受两个参数:一个初始值,以及对该值进行相应操作的指令。Reducer 会根据你提供的指令对初始值应用某种逻辑,并返回一个全新的值。
const reducer = (value, instructions) => newValue
关于 Reducer,需要理解的一件重要事情是它们永远只返回一个值。Reducer 是纯函数,它将原始输入Reduce为单个返回值,而不会改变传入的原始值,并且在给定相同参数的情况下,始终会产生相同的返回值。
JavaScript 中这种模式的一个很好的例子是.reduce()
数组方法。与 类似useReducer
,此方法接受两个参数:一个 Reducer 函数和一个用于应用 Reducer 函数的初始值。
const nums = [1, 2, 3]
const initialValue = 0
const reducer = (accumulator, item) => accumulator + item
const total = nums.reduce(reducer, initialValue)
console.log(nums) // [1, 2, 3]
console.log(total) // 6
在这个例子中,.reduce()
循环遍历我们的数组,并在每次迭代中nums
应用我们的函数。我们希望 Reducer 在第一次迭代时将其用作起始点。是上次调用中返回的收集值,用于通知函数下一个值将添加到哪里。reducer
initialValue
accumulator
第一次迭代: 0 + 1 => 1
第二次迭代: 1 + 2 => 3
第三次迭代: 3 + 3 => 6
该nums
数组被缩减为单个返回值 6。
Reducers 在 React 中如何使用?
在 React 中,Reducer 负责处理应用程序从一个状态到下一个状态的转换。我们提供给 Reducer 的初始值称为当前状态,而我们提供的指令称为动作。
当前状态和动作进入,新状态从另一侧出来。
const reducer = (state, action) => newState
Reducer 函数通过根据动作提供的信息确定要做什么来处理状态转换。
行动
操作表示在整个应用中发生的独特事件。从用户与页面的交互、通过网络请求进行的外部交互,到与设备 API 的直接交互,所有这些事件都可以用操作来描述。
以下是Flux 标准针对动作对象描述的一些动作的一般约定:
行动必须
- 是一个简单的 JavaScript 对象;
- 拥有
type
财产
行动可能
- 有財產
error
。 - 有財產
payload
。 - 有財產
meta
。
操作不得包含除type
、payload
、error
和 之外的属性meta
。
action.type
操作的 向消费者标识type
已发生操作的性质。type
是一个字符串常量。如果两个类型相同,则它们必须严格等效(使用===
)。
// Action with type property
{
type: 'ADD_TODO'
}
action.payload
可选 属性可以是任何类型的值。它表示操作的有效负载。任何与操作无关的信息(操作本身payload
或状态除外)都应包含在该字段中。type
payload
// Action with type and payload properties
{
type: 'ADD_TODO',
payload: {
todo,
completed: false,
id: id()
},
}
action.error
如果操作代表错误,则可以设置可选 属性。error
true
一个为 true 的操作error
类似于被拒绝的 Promise。按照惯例,如果error
为true
,则payload
应该是一个错误对象。这类似于拒绝一个带有错误对象的 Promise。
// Action representing an error. The error property is set to true, therefore the payload is an error object.
{
type: 'ADD_TODO',
payload: new Error(),
error: true
}
action.meta
可选 属性可以是任何类型的值。它用于表示不属于有效载荷的任何额外信息meta
。
调度动作
正如我在开头提到的,初始化时useReducer
会返回一个包含两项的数组。第一项是我们的当前状态,第二项是调度方法。
const [todos, dispatch] = useReducer(reducer, [])
当调用时,此调度方法负责将动作传递给我们的 reducer 函数。
当特定事件发生时,会触发 Actions。以目前为止使用的待办事项应用示例为例,这些事件可以用如下 Actions 来表示:
- 添加待办事项
- 删除待办事项
- 切换待办事项是否完成。
让我们为这些事件创建一些动作类型。
const ADD_TODO = 'ADD_TODO'
const DELETE_TODO = 'DELETE_TODO'
const TOGGLE_COMPLETED = 'TOGGLE_COMPLETED'
在使用这些操作类型时,我们可以在整个应用程序中使用字符串,但通过将它们分配给变量,我们可以避免字符串拼写错误的问题,因为拼写错误不会引发错误,从而浪费时间去追踪错误。如果我们拼错了变量名,我们会收到一条有用的错误消息,告诉我们哪里做错了。
现在让我们添加一些处理函数,这些函数将调用 dispatch 并向其传递一个 action 对象。当某些事件发生时,这些处理函数将被触发。
// calls dispatch, passing it an action object with a type property of ADD_TODO,
// and a payload property containing the todo text that was passed in,
// a default value of false for the completed property, and a unique id.
const addTodo = todo => {
dispatch({
type: ADD_TODO,
payload: {
todo,
completed: false,
id: id()
}
});
};
// calls dispatch, passing it an action object with a type property of DELETE_TODO,
// and accepts an id which is the only property in our payload.
const deleteTodo = id => {
dispatch({
type: DELETE_TODO,
payload: {
id
}
});
};
// calls dispatch, passing it an action object with a type property of TOGGLE_COMPLETED,
// and accepts an id which is the only property in our payload.
const completeTodo = id => {
dispatch({
type: TOGGLE_COMPLETED,
payload: {
id
}
});
};
每个 action 在被调度时,都会由我们的 Reducer 进行不同的处理。Reducer 中常见的一种模式是使用 switch 语句。这并非强制要求,只要我们优化了代码的可读性,任何条件逻辑都可以使用 switch 语句。为了展示 switch 语句以外的内容,以下是一个使用 if-else 语句来处理待办事项应用的 Reducer 的示例。
const todoReducer = (state, action) => {
if (action.type === ADD_TODO) {
return [action.payload, ...state]
}
if (action.type === DELETE_TODO) {
return state.filter(todo => todo.id !== action.payload.id)
}
if (action.type === TOGGLE_COMPLETED) {
return state.map(todo => {
if (todo.id !== action.payload.id) return todo
return {...todo, completed: !todo.completed}
})
}
return state
}
上述减速器知道在给定每种类型的动作时该做什么。
如果分派的操作具有ADD_TODO的类型属性:
- 返回当前状态的副本,将新的待办事项添加到数组的开头。
如果分派的操作具有DELETE_TODO的 type 属性:
- 过滤我们的待办事项列表,返回所有 id 与我们操作的有效负载传递的 id 不匹配的待办事项的新列表,因此从列表中删除待办事项。
如果调度的操作具有TOGGLE_COMPLETED的 type 属性:
- 循环遍历待办事项列表,查找 id 属性与操作负载中的 id 匹配的待办事项。如果不匹配,则按原样返回待办事项。如果找到匹配项,则复制待办事项的属性,并将该
completed
属性替换为与原先相反的值。
如果这些都不成立并且我们收到了无法识别的操作,则按原样返回当前状态。
整合起来
我们已经介绍了如何使用 Reducer Hook 管理更复杂的状态的基本组件。让我们来看一个更实际的示例,了解如何useReducer
在一个典型的联系表单组件中管理状态。
让我们首先构建表单组件的最基本结构。
import React, { useReducer } from 'react'
const Form = () => {
// for now, we will just prevent the default
// behaviour upon submission
handleSubmit = e => {
e.preventDefault()
}
return (
<>
<h1>Send a Message</h1>
<form onSubmit={handleSubmit}>
<label htmlFor='name'>
Name
<input id='name' name='name' type='text' />
</label>
<label htmlFor='email'>
Email
<input id='email' name='email' type='email' />
</label>
<label htmlFor='subject'>
Subject
<input id='subject' name='subject' type='text' />
</label>
<label htmlFor='body'>
Body
<textarea id='body' name='body' />
</label>
<button type='submit'>
Send
</button>
</form>
</>
)
}
export default Form
接下来,我们声明我们的动作类型、表示初始状态的对象以及 Reducer 函数。你可以在组件内部或外部声明它们,也可以将它们写在单独的文件中,并在需要时导入。在本例中,我将在同一个文件中声明它们,但将其放在组件外部,以<Form />
减少代码混乱,更易于阅读。
我们还需要初始化我们的useReducer
钩子,并将我们新创建的 reducer 函数和初始状态对象传递给它。
为了多样化,我将在我们的 reducer 中使用 switch 语句。
import React, { useReducer } from 'react'
// action types
const UPDATE_FIELD_VALUE = 'UPDATE_FIELD_VALUE'
// initial state
const INITIAL_STATE = {
name: '',
email: '',
subject: '',
body: '',
}
// reducer function
const formReducer = (state, action) => {
switch (action.type) {
case UPDATE_FIELD_VALUE:
return { ...state, [action.payload.field]: action.payload.value }
default:
return INITIAL_STATE
}
// form component
const Form = () => {
// initialize useReducer
const [state, dispatch] = useReducer(formReducer, INITIAL_STATE)
...
现在我们需要将输入的控制权交给React,以便我们可以将输入值存储在状态中。
首先,让我们将每个输入的值设置为状态中存储的相应值。
<input
id='name'
name='name'
type='text'
value={state.name}
/>
单独执行此操作将禁用我们的输入,因为我们已将值硬编码为空字符串,而没有有关如何处理更改事件的说明。
因此,我们还需要为输入提供一个onChange
属性并传递给它一个函数,以便我们可以更新状态中存储的值。
<input
id='name'
name='name'
type='text'
value={state.name}
onChange={e => updateFieldValue(e.target.name, e.target.value)}
/>
我们的updateFieldValue
处理函数:
const updateFieldValue = (field, value) => {
dispatch({
type: UPDATE_FIELD_VALUE,
payload: {
field,
value,
},
})
}
现在,当用户在我们的输入字段中输入内容时,updateFieldValue
就会触发该函数,该函数向我们分派一个formReducer
类型为的操作UPDATE_FIELD_VALUE
,以及一个有效负载,其中包括已更新的字段以及该字段的新值。
我们formReducer
知道如何处理这种动作类型并返回具有更新的字段值的新状态。
到目前为止,我们的 Form 组件如下所示:
import React, { useReducer } from 'react'
// initial state values
const INITIAL_STATE = {
name: '',
email: '',
subject: '',
body: '',
}
// action types
const UPDATE_FIELD_VALUE = 'UPDATE_FIELD_VALUE'
// reducer function
const formReducer = (state, action) => {
switch (action.type) {
case UPDATE_FIELD_VALUE:
return { ...state, [action.payload.field]: action.payload.value }
default:
return INITIAL_STATE
}
}
// Form component
const Form = () => {
const [state, dispatch] = useReducer(formReducer, INITIAL_STATE)
// input change handler function
const updateFieldValue = (field, value) => {
dispatch({
type: UPDATE_FIELD_VALUE,
payload: {
field,
value,
},
})
}
// submit handler
const handleSubmit = event => {
event.preventDefault()
}
return (
<>
<h1>Send a Message</h1>
<form onSubmit={handleSubmit}>
<label htmlFor='name'>
Name
<input
id='name'
name='name'
type='text'
value={state.name}
onChange={e => updateFieldValue(e.target.name, e.target.value)}
required
/>
</label>
<label htmlFor='email'>
Email
<input
id='email'
name='email'
type='email'
value={state.email}
onChange={e => updateFieldValue(e.target.name, e.target.value)}
required
/>
</label>
<label htmlFor='subject'>
Subject
<input
id='subject'
name='subject'
type='text'
value={state.subject}
onChange={e => updateFieldValue(e.target.name, e.target.value)}
/>
</label>
<label htmlFor='body'>
Body
<textarea
id='body'
name='body'
type='text'
value={state.body}
onChange={e => updateFieldValue(e.target.name, e.target.value)}
required
/>
</label>
<button type='submit'>
Send
</button>
</form>
</>
)
}
export default Form
我们的表单成功地使用了 Reducer 钩子来更新并跟踪状态中的输入值。现在我们需要处理与提交表单相关的各种状态,并将这些状态显示给用户。
添加表单状态
目前,我们只有一种操作来更新状态中各个输入字段的值。这本身就是 的有效用例useReducer
,但考虑到提交表单所涉及的所有状态,更新和存储输入值只是其中的一小部分。
以下是我们的表单可能存在的一些常见状态:
- 空闲:我们的初始状态。一个空的表单,等待填写和提交;
- 待定:我们已提交表格,正在等待查看提交是否成功;
- 成功:我们的表格已成功提交;
- 错误:尝试发送表单时出现错误;
所有这些表单状态都需要被跟踪并传达给用户。每个状态将由不同的 UI 呈现。
让我们添加一个新的动作类型来表示这些状态变化:
// action types
const UPDATE_FIELD_VALUE = 'UPDATE_FIELD_VALUE'
const UPDATE_STATUS = 'UPDATE_STATUS'
与我们的 action 类型类似,我将为当前的表单状态声明一些新变量,以避免之前提到的使用字符串代替变量的问题。我们希望在出现拼写错误时能够显示有用的错误消息。
// form status variables
const IDLE = 'UPDATE_FIELD_VALUE'
const PENDING = 'PENDING'
const SUCCESS = 'SUCCESS'
const ERROR = 'ERROR'
还向我们的初始状态添加一个新status
属性,其默认值为IDLE
// initial state
const INITIAL_STATE = {
name: '',
email: '',
subject: '',
body: '',
status: IDLE,
}
现在我们需要添加一个新的case
来处理 类型的动作UPDATE_STATUS
。如果一个动作被调度为 类型UPDATE_STATUS
,我们将按原样返回状态的副本,status
并用动作负载中的新值替换属性的值。
// reducer function
const formReducer = (state, action) => {
switch (action.type) {
case UPDATE_FIELD_VALUE:
return { ...state, [action.payload.field]: action.payload.value }
case UPDATE_STATUS:
return { ...state, status: action.payload.status }
default:
return INITIAL_STATE
}
在组件内部Form
,我们添加一个新的处理函数,用于通知UPDATE_STATUS
事件的发生。我们将其命名为 handler updateStatus
。
// Form component
const Form = () => {
const [state, dispatch] = useReducer(formReducer, INITIAL_STATE)
// handler functions
const updateFieldValue = (field, value) => {
dispatch({
type: UPDATE_FIELD_VALUE,
payload: {
field,
value,
},
})
}
const updateStatus = status => {
dispatch({
type: UPDATE_STATUS,
payload: {
status,
},
})
}
...
现在,我们可以为函数添加更新state 中属性handleSubmit
的逻辑了。通常,你会向某个负责处理钩子中传入消息的 API 发送请求。然后,该 API 会通过提供错误响应或成功响应来告知请求是否成功。现在,我们将模拟此功能,首先将 our 设置为,然后在两秒钟后将其值设置为。status
POST
useEffect
status
PENDING
SUCCESS
...
// submit handler
const handleSubmit = event => {
event.preventDefault()
updateStatus(PENDING)
setTimeout(() => {
updateStatus(SUCCESS)
}, 2000)
}
...
现在在我们的表单中,我们可以添加一些标记来向用户显示IDLE
、、和状态PENDING
。SUCCESS
ERROR
...
// Success state
if (state.status === SUCCESS) {
return <p>Your message was sent successfully.</p>
}
// Error state
if (state.status === ERROR) {
return <p>Oops! Something went wrong...</p>
}
// Default State
return (
<>
<h1>Send a Message</h1>
<form onSubmit={handleSubmit}>
<label htmlFor='name'>
Name
<input
id='name'
name='name'
type='text'
value={state.name}
onChange={e => updateFieldValue(e.target.name, e.target.value)}
required
/>
</label>
<label htmlFor='email'>
Email
<input
id='email'
name='email'
type='email'
value={state.email}
onChange={e => updateFieldValue(e.target.name, e.target.value)}
required
/>
</label>
<label htmlFor='subject'>
Subject
<input
id='subject'
name='subject'
type='text'
value={state.subject}
onChange={e => updateFieldValue(e.target.name, e.target.value)}
/>
</label>
<label htmlFor='body'>
Body
<textarea
id='body'
name='body'
type='text'
value={state.body}
onChange={e => updateFieldValue(e.target.name, e.target.value)}
required
/>
</label>
<button type='submit' disabled={state.status === PENDING}>
{state.status !== PENDING ? 'Send' : 'Sending...'}
</button>
</form>
</>
)
}
export default Form
有了这个,在提交表单时,status
设置PENDING
为两秒钟,这将禁用提交按钮并将按钮文本更改为发送...而不是发送。
两秒钟后,status
设置为SUCCESS
,以呈现消息“您的消息已成功发送。”而不是我们的表格。
ERROR
要立即查看消息,您可以status
对ERROR
进行硬编码INITIAL_STATE
,这样将显示消息“哎呀!出了点问题……”而不是我们的表单。
至此,我们已经具备了管理大多数表单状态的基本功能。您仍然需要将提交处理程序替换为实际功能,并编写样式来帮助传达各种表单状态。
唯一缺少的是重置按钮,该按钮允许用户在提交成功或失败时发送另一条消息。为此,我们将利用useReducer
我在本文开头提到的可选的第三个参数。
延迟初始化
useReducer
还使我们能够延迟创建初始状态。为此,你可以传递一个init
函数作为可选的第三个参数。
初始状态将被设置为init(initialState)
。
const [todos, dispatch] = useReducer(reducer, initialState, init);
该init
函数允许你将计算初始状态的逻辑提取到 Reducer 之外。这对于响应某个 Action 并将状态重置为初始值也很方便。
在我们的例子中,此操作将具有类型RESET
,因此让我们为此添加另一种操作类型:
//action types
const UPDATE_FIELD_VALUE = 'UPDATE_FIELD_VALUE'
const UPDATE_STATUS = 'UPDATE_STATUS'
const RESET = 'RESET'
声明我们的 init 函数:
// init function passed as optional 3rd argument for lazy initialization
const init = initialState => initialState
添加一个新案例来处理新的动作类型
// reducer function
const formReducer = (state, action) => {
switch (action.type) {
case UPDATE_FIELD_VALUE:
return { ...state, [action.payload.field]: action.payload.value }
case UPDATE_STATUS:
return { ...state, status: action.payload.status }
case RESET:
return init(INITIAL_STATE)
default:
return INITIAL_STATE
}
}
将我们的 init 函数作为第三个参数传递给useReducer
:
// Form component
...
const Form = () => {
const [state, dispatch] = useReducer(formReducer, INITIAL_STATE, init)
...
添加新的处理函数:
...
const resetForm = () => {
dispatch({ type: RESET })
}
...
最后,更新我们的SUCCESS
UIERROR
以包含一个触发resetForm
处理程序函数的按钮,将表单设置回其原始状态并将其显示给用户。
...
// Success state
if (state.status === SUCCESS) {
return (
<>
<p>Your message was sent successfully.</p>
<button type='button' onClick={resetForm}>
Send Another Message
</button>
</>
)
}
// Error state
if (state.status === ERROR) {
return (
<>
<p>Something went wrong...</p>
<button type='button' onClick={resetForm}>
Try Again
</button>
</>
)
}
...
我们完成的表单组件
import React, { useReducer } from 'react'
// form status variables
const IDLE = 'UPDATE_FIELD_VALUE'
const PENDING = 'PENDING'
const SUCCESS = 'SUCCESS'
const ERROR = 'ERROR'
// initial state values
const INITIAL_STATE = {
name: '',
email: '',
subject: '',
body: '',
status: IDLE,
}
// action types
const UPDATE_FIELD_VALUE = 'UPDATE_FIELD_VALUE'
const UPDATE_STATUS = 'UPDATE_STATUS'
const RESET = 'RESET'
// 3rd parameter for lazy initialization
const init = initialState => initialState
// reducer function
const formReducer = (state, action) => {
switch (action.type) {
case UPDATE_FIELD_VALUE:
return { ...state, [action.payload.field]: action.payload.value }
case UPDATE_STATUS:
return { ...state, status: action.payload.status }
case RESET:
return init(INITIAL_STATE)
default:
return INITIAL_STATE
}
}
// Form component
const Form = () => {
const [state, dispatch] = useReducer(formReducer, INITIAL_STATE, init)
// handler functions
const updateFieldValue = (field, value) => {
dispatch({
type: UPDATE_FIELD_VALUE,
payload: {
field,
value,
},
})
}
const updateStatus = status => {
dispatch({
type: UPDATE_STATUS,
payload: {
status,
},
})
}
const resetForm = () => {
dispatch({ type: RESET })
}
// MOCK submit handler
const handleSubmit = event => {
event.preventDefault()
updateStatus(PENDING)
setTimeout(() => {
updateStatus(SUCCESS)
}, 2000)
}
// Success state UI
if (state.status === SUCCESS) {
return (
<>
<p>Your message was sent successfully.</p>
<button type='button' onClick={resetForm}>
Send Another Message
</button>
</>
)
}
// Error state UI
if (state.status === ERROR) {
return (
<>
<p>Something went wrong...</p>
<button type='button' onClick={resetForm}>
Try Again
</button>
</>
)
}
// Default state UI
return (
<>
<h1>Send a Message</h1>
<form onSubmit={handleSubmit}>
<label htmlFor='name'>
Name
<input
id='name'
name='name'
type='text'
value={state.name}
onChange={e => updateFieldValue(e.target.name, e.target.value)}
required
/>
</label>
<label htmlFor='email'>
Email
<input
id='email'
name='email'
type='email'
value={state.email}
onChange={e => updateFieldValue(e.target.name, e.target.value)}
required
/>
</label>
<label htmlFor='subject'>
Subject
<input
id='subject'
name='subject'
type='text'
value={state.subject}
onChange={e => updateFieldValue(e.target.name, e.target.value)}
/>
</label>
<label htmlFor='body'>
Body
<textarea
id='body'
name='body'
type='text'
value={state.body}
onChange={e => updateFieldValue(e.target.name, e.target.value)}
required
/>
</label>
<button type='submit' disabled={state.status === PENDING}>
{state.status !== PENDING ? 'Send' : 'Sending...'}
</button>
</form>
</>
)
}
export default Form
回顾
useReducer
useState
当您具有涉及多个子值的复杂状态逻辑或下一个状态依赖于前一个状态时,这是更可取的;- 调用时,
useReducer
返回一个包含两项的数组:当前状态和调度方法; useReducer
接受三个参数:一个 reducer 函数、初始状态和用于状态延迟初始化的可选 init 函数;- 在 React 中,Reducer 负责处理应用程序从一个状态到下一个状态的转换。Reducer 接收当前状态和一个 Action,并返回一个全新的状态;
- 动作表达了在整个应用程序中发生的独特事件。
- Flux 标准针对动作对象描述了一些动作的通用约定;
- 当特定事件发生时,动作就会发送给我们的减速器;
感谢阅读!
鏂囩珷鏉ユ簮锛�https://dev.to/brettblox/react-hooks-usereducer-4g3m