# Reduce 术语
# Action
action 是一个具有 type
字段的普通 JavaScript 对象。你可以将 action 视为描述应用程序中发生了什么的事件.
一个典型的 action 对象可能如下所示:
const addTodoAction = {
type: 'todos/todoAdded', // 其中第一部分是这个 action 所属的特征或类别,第二部分是发生的具体事情
payload: 'Buy milk' // 可以添加有关发生的事情的附加信息
}
2
3
4
# Action Creator (opens new window)
action creator 是一个创建并返回一个 action 对象的函数。它的作用是让你不必每次都手动编写 action 对象:
const addTodo = text => {
return {
type: 'todos/todoAdded',
payload: text
}
}
2
3
4
5
6
# Reducer
reducer 是一个函数,接收当前的 state
和一个 action
对象。
函数签名是:(state, action) => newState
。 可以将 reducer 视为一个事件监听器,它根据接收到的 action(事件)类型处理事件。
Reducer 必需符合以下规则:
- 仅使用
state
和action
参数计算新的状态值。 - 禁止直接修改
state
。必须通过复制现有的state
并对复制的值进行更改的方式来做 不可变更新(immutable updates)。 - 禁止任何异步逻辑、依赖随机值或导致其他“副作用”的代码。
下面是 reducer 的小例子,展示了每个 reducer 应该遵循的步骤:
const initialState = { value: 0 }
function counterReducer(state = initialState, action) {
// 检查 reducer 是否关心这个 action
if (action.type === 'counter/increment') {
// 如果是,复制 `state`
return {
...state,
// 使用新值更新 state 副本
value: state.value + 1
}
}
// 返回原来的 state 不变
return state
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Store
当前 Redux 应用的 state 存在于一个名为 store 的对象中。
store 是通过传入一个 reducer 来创建的,并且有一个名为 getState
的方法,它返回当前状态值:
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({ reducer: counterReducer })
console.log(store.getState())
// {value: 0}
2
3
4
5
6
# Dispatch
Redux store 有一个方法叫 dispatch
。更新 state 的唯一方法是调用 store.dispatch()
并传入一个 action 对象。 store 将执行所有 reducer 函数并计算出更新后的 state,调用 getState()
可以获取新 state。
store.dispatch({ type: 'counter/increment' })
console.log(store.getState())
// {value: 1}
2
3
4
dispatch 一个 action 可以形象的理解为 "触发一个事件"。
我们通常调用 action creator 来调用 action:
const increment = () => {
return {
type: 'counter/increment'
}
}
store.dispatch(increment())
console.log(store.getState())
// {value: 2}
2
3
4
5
6
7
8
9
10
# Selector
Selector 函数可以从 store 状态树中提取指定的片段。随着应用变得越来越大,会遇到应用程序的不同部分需要读取相同的数据,selector 可以避免重复这样的读取逻辑:
const selectCounterValue = state => state.value
const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2
2
3
4
5
笔记
类似于 vuex 的计算属性
# 创建 Redux Store
一个可能的配置可能是这样的:
app/store.js
import { configureStore } from '@reduxjs/toolkit'
import usersReducer from '../features/users/usersSlice'
import postsReducer from '../features/posts/postsSlice'
import commentsReducer from '../features/comments/commentsSlice'
import counterReducer from '../features/counter/counterSlice
export default configureStore({
reducer: {
users: usersReducer,
posts: postsReducer,
comments: commentsReducer,
counter: counterReducer
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
其中 counterReducer 模块可能是这样的:
features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit'
// Redux Toolkit 的 createSlice 的函数,负责生成 action 类型字符串、action creator 函数和 action 对象的工作。
// 比如 name + reducers 名称 (counter + increment),生成了一个 action 类型 {type: "counter/increment"}。
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
increment: state => {
// Redux Toolkit 允许我们在 reducers 写 "可变" 逻辑。
// 并不是真正的改变 state 因为它使用了 immer 库
// 当 immer 检测到 "draft state" 改变时,会基于这些改变去创建一个新的(immer使用了proxy来监听变化)
// 不可变的 state
state.value += 1
},
decrement: state => {
state.value -= 1
},
incrementByAmount: (state, action) => {
state.value += action.payload
},
// prepare 函数让我们可以实现自定义 action
// 组件中可以直接使用 dispatch(postAdded(title, content)) 来调度这个 reducer action
postAdded: {
reducer(state, action) {
state.push(action.payload)
},
prepare(title, content) {
return {
payload: {
id: nanoid(),
title,
content
}
}
}
}
}
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
// 异步Thunk
export const incrementAsync = (amount) => (dispatch) => {
setTimeout(() => {
dispatch(incrementByAmount(amount))
}, 1000)
}
// Selector 属性包装
export const selectCount = (state) => state.counter.value
// counterSlice.reducer就是一个通过内置 createReducer 生成的,签名类型为 `(state, action) => newState` 的 reducer 函数
export default counterSlice.reducer
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 基于 proxy 实现的更新不可变逻辑
常见的手动编写不可变的更新逻辑可能是这样的:
// 通过创建原始值的副本的方式来更新不可变数据
function handwrittenReducer(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
所以,上面的代码可以变成这样:
function reducerWithImmer(state, action) {
state.first.second[action.someId].fourth = action.someValue
}
2
3
警告!
你只能在 Redux Toolkit 的
createSlice
和createReducer
中编写 “mutation” 逻辑,因为它们在内部使用 Immer!如果你在没有 Immer 的 reducer 中编写 mutation 逻辑,它将改变状态并导致错误!
# 用 Thunk 编写异步逻辑
目前我们的一系列流程都是同步的:dispatch action,store 调用 reducer 来计算新状态,然后 dispatch 函数完成并结束。
但实际上也有一些 API 请求数据之类的异步逻辑可能需要我们去处理。
thunk 是一种特定类型的 Redux 函数,可以包含异步逻辑。Thunk 是使用两个函数编写的:
- 一个内部 thunk 函数,它以
dispatch
和getState
作为参数 - 外部创建者函数,它创建并返回 thunk 函数
features/counter/counterSlice.js
// 下面这个函数就是一个 thunk ,它使我们可以执行异步逻辑
// 你可以 dispatched 异步 action `dispatch(incrementAsync(10))` 就像一个常规的 action
// 调用 thunk 时接受 `dispatch` 函数作为第一个参数
// 当异步代码执行完毕时,可以 dispatched actions
export const incrementAsync = amount => dispatch => {
setTimeout(() => {
dispatch(incrementByAmount(amount))
}, 1000)
}
2
3
4
5
6
7
8
9
并像使用普通 Redux action creator 一样使用它们:
store.dispatch(incrementAsync(5))
AJAX 调用以从服务器获取数据时,你可以将该调用放入 thunk 中。
// 外部的 thunk creator 函数
const fetchUserById = userId => {
// 内部的 thunk 函数
return async (dispatch, getState) => {
try {
// thunk 内发起异步数据请求
const user = await userAPI.fetchById(userId)
// 当数据响应完成后 dispatch 一个 action
dispatch(userLoaded(user))
// 输出更改后的值
const data = getState()
console.log(data)
} catch (err) {
// 如果过程出错,在这里处理
}
}
}
store.dispatch(fetchUserById(42))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 使用createAsyncThunk
生成 Thunk
Redux Toolkit 的 createAsyncThunk
API 生成 thunk,并能够自动 dispatch 那些 "start/success/failure" action。
createAsyncThunk
接收 2 个参数:
- 将用作生成的 action 类型的前缀的字符串
- 一个 “payload creator” 回调函数,它应该返回一个包含一些数据的
Promise
,或者一个被拒绝的带有错误的Promise
features/posts/postsSlice
import { createSlice, nanoid, createAsyncThunk } from '@reduxjs/toolkit'
import { client } from '../../api/client'
// 这里我们用了额外的字段来标识数据状态
// 如 status:'idle' | 'loading' | 'succeeded' | 'failed',
const initialState = {
posts: [],
status: 'idle',
error: null
}
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
// omit existing reducers here
},
// 在这个例子中,我们需要监听我们 fetchPosts thunk dispatch 的 "pending" 和 "fulfilled" action 类型。这些 action creator 附加到我们实际的 fetchPost 函数中,我们可以将它们传递给 extraReducers 来监听这些 action
extraReducers(builder) {
builder
.addCase(fetchPosts.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = 'succeeded'
// Add any fetched posts to the array
state.posts = state.posts.concat(action.payload)
})
.addCase(fetchPosts.rejected, (state, action) => {
state.status = 'failed'
state.error = action.error.message
})
}
})
// 传入 'posts/fetchPosts' 作为 action 类型的前缀
// 我们希望我们 dispatch 的 Redux action 有一个 payload,也就是 posts 数组。所以,我们提取 response.data,并从回调中返回它。
export const fetchPosts = createAsyncThunk('posts/fetchPosts', async () => {
const response = await client.get('/fakeApi/posts')
return response.data
})
// 当调用 dispatch(fetchPosts()) 的时候,fetchPosts 这个 thunk 会首先 dispatch 一个 action 类型为'posts/fetchPosts/pending'
// 我们可以在我们的 reducer 中监听这个 action 并将请求状态标记为 “loading 正在加载”。
// 一旦 Promise 成功,fetchPosts thunk 会接受我们从回调中返回的 response.data 数组,并 dispatch 一个 action,action 的 payload 为 posts 数组,action 的 类型为 'posts/fetchPosts/fulfilled'。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
createAsyncThunk
,只能传递一个参数,无论我们传入的是什么,它都将成为 payload creation 回调的第一个参数。
payload creator 的第二个参数是一个' thunkAPI '对象,包含几个有用的函数和信息:
dispatch
和getState
:dispatch
和getState
方法由 Redux store 提供。你可以在 thunk 中使用这些来发起 action,或者从最新的 Redux store 中获取 state (例如在发起 另一个 action 后获取更新后的值)。extra
:当创建 store 时,用于传递给 thunk 中间件的“额外参数”。这通常时某种 API 的包装器,比如一组知道如何对应用程序的服务器进行 API 调用并返回数据的函数,这样你的 thunk 就不必直接包含所有的 URL 和查询逻辑。requestId
:该 thunk 调用的唯一随机 ID ,用于跟踪单个请求的状态。signal
:一个AbortController.signal
函数,可用于取消正在进行的请求。rejectWithValue
:一个用于当 thunk 收到一个错误时帮助自定义rejected
action 内容的工具。
(如果你要手写 thunk 而不是使用 createAsyncThunk
,则 thunk 函数将获取 (dispatch, getState)
作为单独的参数,而不是将他们放在一个对象中。)
# 在应用程序中加入Redux
在这里使用了一个名为 <Provider>
的组件在幕后传递 Redux store,以便他们可以访问 useSelector 等相关 hook。
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import store from './app/store'
import { Provider } from 'react-redux'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
2
3
4
5
6
7
8
9
10
11
12
13
# 组件中使用 Redux
features/counter/Counter.js
import React, { useState, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import {
decrement,
increment,
incrementByAmount,
incrementAsync,
selectCount
} from './counterSlice'
import styles from './Counter.module.css'
export function Counter() {
// 因为我们无法直接访问store,可以使用 useSelector 这个 hooks ,让我们的组件从 Redux 的 store 状态树中提取它需要的任何数据。
// 比如 selectCount 就是一个导出的计算属性
const count = useSelector(selectCount)
// 当然也可以直接写逻辑
const countPlusTwo = useSelector(state => state.counter.value + 2)
const postStatus = useSelector(state => state.posts.status)
const dispatch = useDispatch()
const [incrementAmount, setIncrementAmount] = useState('2')
// dispatch thunk(异步 action,调用 api 初始化数据)
useEffect(() => {
if (postStatus === 'idle') {
dispatch(fetchPosts())
}
}, [postStatus, dispatch])
return (
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
+
</button>
<span className={styles.value}>{count}</span>
<button
className={styles.button}
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
-
</button>
<button
className={styles.button}
aria-label="Increment value by 20"
onClick={() => dispatch(incrementByAmount(20))}
>
+20
</button>
</div>
{/* 这里省略了额外的 render 代码 */}
</div>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59