# Redux 学习笔记
Redux 是 JavaScript 应用的状态容器,提供可预测的状态管理。一般用于 React 项目中,用于实现全局状态管理。
# Redux Core
首先我们介绍 Redux 的核心概念
# 安装
# NPM
$ npm install redux
# Yarn
$ yarn add redux
2
3
4
5
# 使用 Redux
下面是使用 Redux 的基本流程
// 1. 创建 Reducer
const bankReducer = (state = 0, action) => {
switch (action.type) {
case 'deposit':
return state + action.payload
case 'withdraw':
return state - action.payload
default:
return state
}
}
// 2. 创建 Store
import { createStore } from 'redux'
const store = createStore(bankReducer)
// 3. Dispatch,相当于向 Store 发送一个事件
button.addEventListener('click', () => {
store.dispatch({ type: 'deposit', payload: 10 })
})
// 4. 订阅状态变化
store.subscribe(() => {
// 5. 获取 state,修改元素值
const state = store.getState()
label.innerHTML = state.toString()
})
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
# 数据流
一张图胜过千言万语
# 核心概念
# Reducer
reducer 是一个函数,接收当前的 state
和一个 action
对象,然后返回新状态。函数签名是:(state, action) => newState
。 你可以将 reducer 视为一个事件监听器,它根据接收到的 action 类型处理事件。
const bankReducer = (state = 0, action) => {
switch (action.type) {
case 'deposit':
return state + action.payload
case 'withdraw':
return state - action.payload
default:
return state
}
}
2
3
4
5
6
7
8
9
10
# Action
action 是一个具有 type
字段的普通 JavaScript 对象。你可以将 action 视为描述应用程序中发生了什么的事件。
type
字段是一个字符串,描述事件的类型,action 对象可以有其它字段,包含事件的附加信息,比如 payload
。action 对象采用 Flux Standard (opens new window)。
{ type: 'deposit', payload: 10 }
# Store
当前 Redux 应用的状态存在于一个名为 Store 的对象中,通过传入一个 reducer 来创建的 store。
const store = createStore(bankReducer)
Store 对象有三个方法:
# getState
获取当前的 state
store.getState()
# dispatch
更新 state 的唯一方法是调用 dispatch()
并传入一个 action 对象。 store 将执行 reducer 函数并计算出更新后的 state.
store.dispatch({ type: 'deposit', payload: 10 })
dispatch 一个 action 可以形象的理解为 "触发一个事件"。
# subscribe
通过 subscribe
添加一个监听器,当 state 发生变化时,会触发这个监听器。你可以在监听器里调用 getState()
获取当前 state。在 React 项目中可以执行 render
来重绘组件。
store.subscribe(() => {
const state = store.getState()
})
2
3
# Action Creator
action creator 是一个创建并返回一个 action 对象的函数。它的作用是让你不必每次都手动编写 action 对象:
const addTodo = text => {
return {
type: 'todos/todoAdded',
payload: text
}
}
2
3
4
5
6
# Selector
Selector 函数可以从 store 状态树中提取指定的部分。随着应用变得越来越大,会遇到应用程序的不同部分需要读取相同的数据,selector 可以避免重复这样的读取逻辑。
const todoList = state => state.todos
# Combining Reducers
当一个 Redux 项目越来越大时,需要根据 state 树中不同的部分将 reducer 分割成很多个小的 reducer。每个小的 reducer 负责更新 state 树中的一部分内容。然后通过 combineReducers
(opens new window) 合并成 root reducer。
作为对比下面是没有使用 combineReducers
的代码
import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'
export default function rootReducer(state = {}, action) {
// always return a new object for the root state
return {
// the value of `state.todos` is whatever the todos reducer returns
todos: todosReducer(state.todos, action),
// For both reducers, we only pass in their slice of the state
filters: filtersReducer(state.filters, action)
}
}
const store = createStore(rootReducer)
2
3
4
5
6
7
8
9
10
11
12
13
14
使用 combineReducers
大大减少了样板代码
import { combineReducers } from 'redux'
import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'
const rootReducer = combineReducers({
todos: todosReducer,
filters: filtersReducer
})
const store = createStore(rootReducer)
2
3
4
5
6
7
8
9
10
11
# Enhancers
增强器,通过替换 Store 对象的 dispatch
、getState
和 subscribe
方法,对 Store 进行一些额外的操作,比如想在每次 dispatch 时,输出 action 日志
增强器的函数签名是:
(createStore) => (rootReducer, preloadedState, enhancers) => new Store
const logOnDispatch = (createStore) => (rootReducer, preloadedState, enhancers) => {
const store = createStore(rootReducer, preloadedState, enhancers)
function newDispatch(action) {
const result = store.dispatch(action)
console.log(action)
return result
}
return { ...store, dispatch: newDispatch }
}
// 然后在创建 Store 时,将 enhancer 传入第三个参数上
const store = createStore(rootReducer, undefined, logOnDispatch)
// 如果没有初始值 `preloadedState`,可以将 enhancer 传入第二个参数上
const store = createStore(rootReducer, logOnDispatch)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
或者每次获取 state 时,添加额外的属性 meaningOfLife
const includeMeaningOfLife = (createStore) => (rootReducer, preloadedState, enhancers) => {
const store = createStore(rootReducer, preloadedState, enhancers)
function newGetState() {
return {
...store.getState(),
meaningOfLife: 42,
}
}
return { ...store, getState: newGetState }
}
2
3
4
5
6
7
8
9
10
11
12
# compose
如果同时添加多个 enhancer 怎么办呢?Redux 提供了 compose
(opens new window) 方法
import { compose } from 'redux'
const composedEnhancer = compose(logOnDispatch, includeMeaningOfLife)
const store = createStore(rootReducer, undefined, composedEnhancer)
2
3
4
# Middleware
中间件在 dispatch action 和 action 到达 reducer 之间提供了一个第三方扩展。我们可以使用 Redux middleware 进行日志记录、崩溃报告、异步 API 请求、路由等。和 reducer 不一样,middleware 可以有 side effects (opens new window)。
Middleware 的函数签名:(storeAPI) => (next) => (action) => {}
const exampleMiddleware = storeAPI => next => action => {
// pass the action onwards with next(action),
// or restart the pipeline with storeAPI.dispatch(action)
return next(action)
}
2
3
4
5
middleware 解析:
storeAPI
: 一个包含 Storedispatch
和getState
方法的对象,即 { dispatch, getState },非 Store 对象next
:下一个 middleware 或者是初始的store.dispath
方法- 调用
next(action)
将 action 传给下一个 middleware 或者初始的store.dispath
方法 - 调用
storeAPI.dispatch
重启 pipeline - 应用 middleware 之后,
store.dispath
返回的是第一个middleware 返回的值,初始的store.dispath
方法返回 action 对象
# 定义 middleware
export const print1 = (storeAPI) => (next) => (action) => {
console.log('1')
return next(action)
}
export const print2 = (storeAPI) => (next) => (action) => {
console.log('2')
return next(action)
}
export const print3 = (storeAPI) => (next) => (action) => {
console.log('3')
return next(action)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 使用 middleware
Redux 通过 applyMiddleware
(opens new window) 将多个 middleware 合成一个 enhancer
import { applyMiddleware } from 'redux'
import { print1, print2, print3 } from './middleware'
const middlewareEnhancer = applyMiddleware(print1, print2, print3)
const store = createStore(rootReducer, middlewareEnhancer)
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })
// log: '1'
// log: '2'
// log: '3'
2
3
4
5
6
7
8
9
10
# Pipeline
Redux middleware 围绕 store.dispath
方法形成一个 pipeline.
# React Devtools
为了让 Redux DevTools (opens new window) 能调试我们的 Store,我们需要添加 redux-devtools-extension
# 安装
npm install --save @redux-devtools/extension
# 配置
使用 composeWithDevTools
代替之前的 Redux 的 compose
方法
import { composeWithDevTools } from 'redux-devtools-extension'
const composedEnhancer = composeWithDevTools(
// 这里可以添加任意 middleware
applyMiddleware(print1, print2, print3)
// 这里可以添加任意 enhancer
// ...
)
const store = createStore(rootReducer, composedEnhancer)
2
3
4
5
6
7
8
9
10
# Thunk
Redux 使用 middleware 来处理异步逻辑,比如网络请求
我们首先写一个异步函数 middleware
const asyncFunctionMiddleware = storeAPI => next => action => {
if (typeof action === 'function') {
// 如果 action 是函数,则直接调用 action 函数,参数是 dispatch 和 getState 方法
return action(storeAPI.dispatch, storeAPI.getState)
}
// 否则传给下一个 middleware
return next(action)
}
2
3
4
5
6
7
8
9
然后我们可以这样使用这个 middleware
const middlewareEnhancer = applyMiddleware(asyncFunctionMiddleware)
const store = createStore(rootReducer, middlewareEnhancer)
// Write a function that has `dispatch` and `getState` as arguments
const fetchSomeData = (dispatch, getState) => {
// Make an async HTTP request
client.get('todos').then(todos => {
// Dispatch an action with the todos we received
dispatch({ type: 'todos/todosLoaded', payload: todos })
// Check the updated store state after dispatching
const allTodos = getState().todos
console.log('Number of todos after loading: ', allTodos.length)
})
}
// 这里的 action 是函数,即 thunk
store.dispatch(fetchSomeData)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
加入异步逻辑之后,Redux 的数据流是这样的
# Redux Thunk
为了简化异步函数 middleware 的创建,Redux 提供了官方的 thunk 库 - Redux Thunk (opens new window)
# 安装
$ npm install redux-thunk
# 配置
import thunkMiddleware from 'redux-thunk'
const composedEnhancer = composeWithDevTools(applyMiddleware(thunkMiddleware))
const store = createStore(rootReducer, composedEnhancer)
2
3
4
有了 redux-thunk,我们就可以写 Thunk 函数了。Thunk 函数可以包含任何我们想要的异步逻辑
function thunkFunction = (dispatch, getState) => {
// 可以 dispatch action
// 还可以 get state
}
2
3
4
上面的 fetchSomeData
就是一个典型的 Thunk 函数
The word "thunk" is a programming term that means "a piece of code that does some delayed work" (opens new window). For more details on how to use thunks, see the thunk usage guide page:
as well as these posts:
# Memoized Selectors
如果使用下面的 selector 从 state 挑选需要的数据,因为 array.map()
总是返回一个新的数组,在每次 dispatch 后都会运行 React-Redux useSelector
hook,返回的新数组将导致组件重新渲染(即使数组的值没有变化),影响性能
const selectTodoIds = state => state.todos.map(todo => todo.id)
为了解决这个问题,可以使用 Reselect (opens new window) 库提供的 createSelector
方法
# 安装
$ npm install reselect
# 使用
import { createSelector } from 'reselect'
import { useSelector } from 'react-redux'
export const selectTodoIds = createSelector(
state => state.todos,
todos => todos.map(todo => todo.id)
)
const todoIds = useSelector(selectTodoIds)
2
3
4
5
6
7
8
9
createSelector
接收多个 input 函数和一个 output 函数,input 函数的返回值作为 output 函数的参数,当 input 函数的返回值没有变化时,output 函数不会运行,重用上一次 output 函数的返回值。
# Normalized State
出于 性能考虑 (opens new window), Redux 项目一般采用 normalized state (opens new window) 存储数据,什么是 normalized state?
- 我们 state 中的每个特定数据只有一个副本,不存在重复。
- 数据保存在查找表中,其中项目 ID 是键,项本身是值。
- 也可能有一个特定项用于保存所有 ID 的数组。
下面这个就是 normalized state
{
users: {
ids: ["user1", "user2", "user3"],
entities: {
"user1": {id: "user1", firstName, lastName},
"user2": {id: "user2", firstName, lastName},
"user3": {id: "user3", firstName, lastName},
}
}
}
2
3
4
5
6
7
8
9
10
# Redux Toolkit
Redux 推荐使用 Redux Toolkit 来简化 Redux 操作,减少样板代码。
- 集成了 Redux Thunk (opens new window)、Reselect (opens new window) 和 Redux Devtools Extension (opens new window),简化了对 Store 的配置
- 使用 Immer (opens new window),简化了 reducer 更新 state 的操作
createAsyncThunk()
,简化异步请求createEntityAdapter()
实现 Normalized State
# 安装
npm install @reduxjs/toolkit
# configureStore()
使用 configureStore()
简化 store 的配置流程
import { configureStore } from '@reduxjs/toolkit'
import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'
const store = configureStore({
reducer: {
// Define a top-level state field named `todos`, handled by `todosReducer`
todos: todosReducer,
filters: filtersReducer
}
})
2
3
4
5
6
7
8
9
10
11
12
13
对比 Redux 的 createStore()
, configureStore()
做了下面这些事情
- 将多个小的 reducers 组合成一个 roote reducer,而
createStore()
需要使用combineReducers()
来组合 reducers - 自动添加了 Thunk middleware
- 自动建立了 Redux DevTools Extension 连接
- 自动添加了更多的 middleware 来检查常见错误,比如意外改变 state
# createSlice()
使用 createSlice()
简化 reducer。
- 使用函数替换
switch/case
语句 - 内部使用了 Immer (opens new window),可以直接修改 state
- 自动生成 action creators
import { createSlice } from '@reduxjs/toolkit'
const initialState = []
const todosSlice = createSlice({
name: 'todos', // action type 的前缀
initialState, // 初始值
reducers: {
todoAdded(state, action) {
// 可以直接修改 state
state.push(action.payload)
},
todoToggled(state, action) {
const todo = state.find(todo => todo.id === action.payload)
todo.completed = !todo.completed
},
}
})
// 自动生成 action creators
export const { todoAdded, todoToggled } = todosSlice.actions
// 导出 reducer
export default todosSlice.reducer
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
自动生成的 action creators 是什么样的呢?
console.log(todoToggled(42))
// {type: 'todos/todoToggled', payload: 42}
2
action.type
是 slice 的 name + 方法名
,而 action.payload
是函数参数
# createAsyncThunk()
Redux Toolkit 集成了 Redux Thunk (opens new window),我们可以直接通过 Thunk 函数书写异步逻辑。
同时 Redux Toolkit 提供了 createAsyncThunk()
方法简化了异步请求。
因为一般的异步请求可以概括为三个状态
- 请求中:pending
- 请求成功:fulfilled
- 请求失败:rejected
import { createAsyncThunk } from '@reduxjs/toolkit'
export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
const response = await client.get('/fakeApi/todos')
return response.todos
})
2
3
4
5
6
createAsyncThunk()
方法接收两个参数
- 生成的 action type 的前缀(
todos/fetchTodos
) - 一个回调函数执行网络请求,要求返回一个 Promise,一般我们使用 async function。这个函数有两个参数
- 第一个是
createAsyncThunk()
返回的函数传入的参数。 - 第二个是
{getState, dispatch}
- 第一个是
createAsyncThunk()
返回一个 thunk 函数.
store.dispatch(fetchTodos())
createAsyncThunk()
自动生成三个 action creator.
fetchTodos.pen
ding:
todos/fetchTodos/pending`fetchTodos.fulfilled
:todos/fetchTodos/fulfilled
fetchTodos.rejected
:todos/fetchTodos/rejected
因为生成的 ation creators 不在 createSlice
里,因此需要通过在 extraReducers
option 使用 builder.addCase(actionCreator, caseReducer)
,监听这些 action types。
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
// omit reducer cases
},
extraReducers: builder => {
builder
.addCase(fetchTodos.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.entities = action.payload
state.status = 'succeeded'
})
.addCase(fetchTodos.rejected, (state, action) => {
state.status = 'failed'
state.error = action.error.message
})
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
builder
对象提供了一些方法,让我们可以定义额外的 case reducer,这些 reducer 将响应在 slice 之外定义的 action:
builder.addCase(actionCreator, reducer)
:定义一个 case reducer,它响应 RTK action creator 生成或者普通字符串定义的 action。builder.addMatcher(matcher, reducer)
:定义一个 case reducer,它可以响应任何matcher
函数返回true
的 action.builder.addDefaultCase(reducer)
:定义一个 case reducer,如果没有其他 case reducer 被执行,这个 action 就会运行。
您可以将这些链接在一起,例如builder.addCase().addCase().addMatcher().addDefaultCase()
。 如果多个匹配器匹配操作,它们将按照定义的顺序运行。
# createEntityAdapter()
Redux Toolkit 提供 createEntityAdapter()
方法实现 Normalized State,它获取集合并将它们放入 { ids: [], entities: {} }
的结构中。
createEntityAdapter()
返回一个 adapter 对象,这个对象包含多个预置函数,用来处理一些常见的情况,这些函数可以用于 reducer 函数中。
addOne
/addMany
: 添加一个或多个 itemsupdateOne
/updateMany
: 更新一个或多个 itemsupsertOne
/upsertMany
: 添加或更新一个或多个 itemsremoveOne
/removeMany
: 根据 ids 删除一个或多个 itemssetAll
: 替换所有的数据
此外 adapter 对象还有两个函数
getInitialState
: 返回对象{ ids: [], entities: {} }
,可以传入参数,将参数组合到这个对象里getSelectors
: 生成标准的 selector 函数- selectAll:返回所有的 items
- selectById:返回 id 对应的 item
- selectIds:返回所有的 ids
# Demo
import { createEntityAdapter } from '@reduxjs/toolkit'
// `createEntityAdapter` 接收一个比较函数,用于数组排序
const postsAdapter = createEntityAdapter({
sortComparer: (a, b) => b.date.localeCompare(a.date)
})
// 返回 { ids: [], entities: {}, status: 'idle', error: null }
const initialState = postsAdapter.getInitialState({
status: 'idle',
error: null
})
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
// omit reducer cases
},
extraReducers(builder) {
// omit other reducers
builder
.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = 'succeeded'
// 使用 postsAdapter.upsertMany 添加或更新多个 items
postsAdapter.upsertMany(state, action.payload)
})
// 使用 postsAdapter.addOne 添加一个 item
.addCase(addNewPost.fulfilled, postsAdapter.addOne)
}
})
export const {
selectAll: selectAllPosts,
selectById: selectPostById,
selectIds: selectPostIds
} = postsAdapter.getSelectors(state => state.posts)
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
# React Redux
React Redux (opens new window) 是官方的 React UI 绑定层。它允许 React 组件从 Redux store 读取数据、dispatch action 去更新 state。
# 安装
$ npm install react-redux
# 注入 Store
React Redux 要求组件不能引入 store 对象,而是通过 Provider
组件注入到整个应用中,这样在组件里就可以使用 React Redux 提供的 hooks 访问 store 了。
// in src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import App from './App'
import store from './store'
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# useStore()
React Redux 使用 useStore()
获取所需注入的 store
import { useStore } from 'react-redux'
const store = useStore()
2
# useSelector()
React Redux 使用 useSelector()
获取所需的 state 数据,useSelector()
接收 selector,返回 state 数据。
import React from 'react'
import { useSelector } from 'react-redux'
import TodoListItem from './TodoListItem'
const selectTodos = state => state.todos
const TodoList = () => {
const todos = useSelector(selectTodos)
const renderedListItems = todos.map(todo => {
return <TodoListItem key={todo.id} todo={todo} />
})
return <ul className="todo-list">{renderedListItems}</ul>
}
export default TodoList
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
每次 dispatch action 时,useSelector()
将重新执行,如果返回的值发生变化,则强制 React 组件重新绘制。useSelector()
使用 ===
进行 reference 标记,如果 useSelector()
返回不同的 reference,将影响性能,例如
const todoIds = useSelector(state => state.todos.map(todo => todo.id))
一个解决办法是使用 Redux 提供的 shallowEqual()
方法,比较数组内容是否发送变化。
import React from 'react'
import { useSelector, shallowEqual } from 'react-redux'
import TodoListItem from './TodoListItem'
const selectTodoIds = state => state.todos.map(todo => todo.id)
const TodoList = () => {
const todoIds = useSelector(selectTodoIds, shallowEqual)
const renderedListItems = todoIds.map(todoId => {
return <TodoListItem key={todoId} id={todoId} />
})
return <ul className="todo-list">{renderedListItems}</ul>
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
另一个解决办法是使用 Memoized Selectors
# useDispatch()
React Redux 使用 useDispatch()
dispatch action
import React from 'react'
import { useDispatch } from 'react-redux'
const Header = () => {
const dispatch = useDispatch()
const onClick = () => {
dispatch({ type: 'todos/add', payload: 10 })
}
return (
<button onClick={onClick}>Dispath</button>
)
}
export default Header
2
3
4
5
6
7
8
9
10
11
12
13
14
15