在刚开始学习 React 时,我们通常使用 useState 管理组件内部的数据.
比如一个最简单的计数器:
Kotlin
import { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
return (
<div>
<button onClick={() => setCount(count - 1)}>-</button>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
)
}
它的数据流非常直接:
javascript
点击按钮
↓
setCount()
↓
count 改变
↓
组件重新渲染
对于一个小组件,这已经够用了。
但随着项目变大,会出现一个问题:
App
├── Header
├── User
├── Product
├── Cart
├── Channel
└── ...
很多组件可能都要使用同一份数据。
比如:
用户信息
购物车数量
登录状态
频道列表
主题配置
这时候,如果这些数据全部靠 props 一层一层传递,就会越来越麻烦。
Redux 的作用,可以先用一句话概括:
Redux 把需要共享的数据集中放到一个公共仓库中管理。
这个公共仓库就是:
store
一、先别背 Redux 名词,先理解它改变了什么
普通 React:
组件
↓
直接修改状态
↓
状态变化
例如:
setCount(count + 1)
Redux 的思想稍微绕了一层:
组件
↓
告诉 Redux:"我要加一"
↓
Redux 根据规定好的逻辑修改数据
↓
得到新状态
也就是说:
组件不直接修改公共数据,而是先发出一个"我要干什么"的消息。
比如:
我要加一
我要减一
我要删除商品
我要保存用户信息
然后 Redux 再根据规则处理。
这就是理解 Redux 最重要的起点。
二、Redux 最核心的四个概念
Redux 最重要的是:
state
action
reducer
store
可以先这样记:
| 名称 | 含义 |
|---|---|
state |
当前数据 |
action |
描述"我要做什么" |
reducer |
根据 state 和 action 决定怎么修改 |
store |
保存和管理 Redux 数据的仓库 |
下面用计数器把它们串起来。
三、state:当前的数据
假设我们有一个计数器:
{
count: 0
}
这就是 state。
它表示当前 Redux 里的状态:
state = {
count: 0
}
点击加号以后可能变成:
state = {
count: 1
}
所以:
state就是"现在的数据是什么"。
四、action:描述"我要做什么"
假设用户点击 +。
组件不会直接写:
state.count++
而是告诉 Redux:
我要执行"增加"操作。
可以使用一个对象描述:
{
type: 'INCREMENT'
}
这个对象就是:
action
所以:
const action = {
type: 'INCREMENT'
}
注意:
action 本身并不会修改数据。
它只是一个描述。
类似于你给 Redux 发了一张命令单:
命令:INCREMENT
到底怎么加,是 reducer 决定的。
五、reducer:真正决定数据怎么变
reducer 是一个函数。
最基本的形式:
function reducer(state, action) {
}
它接收两个参数:
state
当前数据
action
这次想做什么
例如:
function reducer(state = { count: 0 }, action) {
if (action.type === 'INCREMENT') {
return {
count: state.count + 1
}
}
if (action.type === 'DECREMENT') {
return {
count: state.count - 1
}
}
return state
}
假设当前:
state = {
count: 0
}
收到:
action = {
type: 'INCREMENT'
}
那么:
action.type === 'INCREMENT'
成立。
于是返回:
{
count: 1
}
所以 Redux 最核心的计算关系其实就是:
旧 state
+
action
↓
reducer
↓
新 state
或者写成:
state + action → reducer → newState
六、store:真正保存 Redux 数据的仓库
有了 reducer 后,可以创建 store。
早期 Redux 中:
const store = createStore(reducer)
可以理解为:
根据 reducer 创建一个 Redux 仓库,这个仓库叫
store。
为什么创建 store 时要传:
reducer
?
因为 store 必须知道:
以后收到 action 后,到底应该按照什么规则修改数据?
这个规则就是 reducer。
所以:
reducer
↓
createStore(reducer)
↓
store
七、dispatch:把 action 发给 Redux
如果想让 count 加一,需要:
store.dispatch({
type: 'INCREMENT'
})
dispatch 可以先理解成:
发送 action。
也就是:
dispatch
=
发命令
整个过程:
store.dispatch({
type: 'INCREMENT'
})
↓
Redux 收到 action
↓
把当前 state 和 action 交给 reducer
↓
reducer(state, action)
↓
生成新的 state
↓
store 保存
所以:
action 负责描述,dispatch 负责发送,reducer 负责真正计算新状态。
八、getState:获取 Redux 当前数据
如果想看看仓库里现在的数据:
store.getState()
比如:
console.log(store.getState())
可能得到:
{
count: 1
}
所以可以简单记:
dispatch → 发数据修改指令
getState → 拿当前数据
九、原生 Redux 的完整流程
现在把前面的知识连起来:
用户点击 +
↓
dispatch({
type: 'INCREMENT'
})
↓
action
{
type: 'INCREMENT'
}
↓
reducer(state, action)
↓
return {
count: state.count + 1
}
↓
store 保存新的 state
Redux 最核心的数据流就是这么简单。
十、Redux Toolkit 是什么
原生 Redux 思想并不复杂,但是代码写起来比较繁琐。
所以现在实际 React 项目中通常使用:
Redux Toolkit
简称:
RTK
它不是另一套状态管理。
它本质上还是 Redux,只不过:
帮你把 Redux 写得更简单。
常见功能包括:
configureStore
createSlice
异步 thunk
Immer
所以:
Redux = 核心思想
Redux Toolkit = 更方便地写 Redux
十一、react-redux 又是什么
另外还有一个:
react-redux
它和 Redux Toolkit 不是一回事。
可以这样理解:
Redux Toolkit
↓
帮助你写 Redux
react-redux
↓
帮助 React 组件使用 Redux
比如 React 组件要读取 Redux:
useSelector()
React 组件要发送 action:
useDispatch()
这些都是:
react-redux
提供的。
因此:
Redux
↑
react-redux
↓
React组件
十二、项目中的 store 目录为什么要拆分
实际项目通常会这样设计:
store
├── modules
│ ├── counterStore.js
│ └── channelStore.js
│
└── index.js
含义:
counterStore.js
↓
只负责计数器
channelStore.js
↓
只负责频道数据
index.js
↓
把所有模块组合成最终 store
这样就不会把所有 Redux 逻辑都堆在一个文件中。
十三、createSlice:Redux Toolkit 的核心
来看计数器模块:
javascript
import { createSlice } from '@reduxjs/toolkit'
const counterStore = createSlice({
name: 'counter',
initialState: {
count: 0
},
reducers: {
increment(state) {
state.count++
},
decrement(state) {
state.count--
}
}
})
这里:
createSlice()
可以理解成:
帮我创建一个 Redux 子模块。
它里面主要有三个东西。
1. name
name: 'counter'
表示:
这个模块叫 counter。
Redux Toolkit 后面生成 action type 时会用到它。
2. initialState
initialState: {
count: 0
}
表示初始状态:
{
count: 0
}
3. reducers
reducers: {
increment(state) {
state.count++
},
decrement(state) {
state.count--
}
}
这里定义:
不同操作应该怎么修改 state。
例如:
increment(state) {
state.count++
}
真正表达的是:
如果执行 increment 操作,就让 count + 1。
十四、为什么 Redux Toolkit 可以直接写 state.count++
前面原生 Redux 通常要求:
return {
count: state.count + 1
}
而 Redux Toolkit 却可以:
state.count++
原因是 RTK 内部使用了:
Immer
所以你表面上看起来像直接修改 state:
state.count++
实际上 Immer 会帮你生成新的不可变 state。
因此在 createSlice 的 reducers 中,这种写法是正常的。
十五、最容易混乱的地方:两个 increment
这是我们学习过程中最容易乱的一个地方。
你写了:
reducers: {
increment(state) {
state.count++
}
}
然后 Redux Toolkit 又会自动生成:
counterStore.actions.increment
于是看起来像有两个 increment。
它们虽然名字一样,但不是一个东西。
可以这样理解。
reducers 里的 increment
increment(state) {
state.count++
}
它的职责是:
真正修改 state。
可以把它脑补成:
执行加一逻辑
actions 里的 increment
counterStore.actions.increment
它是 RTK 自动生成的:
action creator。
它的职责不是修改 state。
它只是:
创建一个 action 对象。
调用:
increment()
会得到类似:
{
type: 'counter/increment'
}
所以它只是发出:
我要 increment。
真正执行:
state.count++
的还是 reducer。
整个关系:
actions.increment()
↓
生成"我要加一"的 action
dispatch(...)
↓
把 action 发出去
reducers.increment(state)
↓
真正执行加一
这是理解 Redux Toolkit 非常关键的一点。
十六、action creator 是什么
所谓:
action creator
直译就是:
action 创建器。
它就是一个专门生成 action 对象的函数。
比如:
increment()
可能返回:
{
type: 'counter/increment'
}
所以:
dispatch(increment())
本质上相当于:
dispatch({
type: 'counter/increment'
})
Redux Toolkit 只是帮我们自动生成了 action。
十七、导出 action 和 reducer
通常会这样写:
const {
increment,
decrement
} = counterStore.actions
这是对象解构。
因为:
counterStore.actions
大概类似:
{
increment: 一个 action creator,
decrement: 一个 action creator
}
然后:
const counterReducer = counterStore.reducer
得到这个模块的 reducer。
最终:
export { increment, decrement }
export default counterReducer
这里可以记:
increment / decrement
↓
给组件 dispatch 用
counterReducer
↓
给 configureStore 用
十八、configureStore:组合 Redux 模块
store/index.js:
javascript
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './modules/counterStore'
const store = configureStore({
reducer: {
counter: counterReducer
}
})
export default store
这里:
counter: counterReducer
表示:
把 counter 模块加入整个 Redux store。
最终 Redux 总 state 的结构大概是:
{
counter: {
count: 0
}
}
所以:
state.counter
得到:
{
count: 0
}
继续:
state.counter.count
得到:
0
十九、useSelector:React 组件读取 Redux 数据
在 React 中:
const { count } = useSelector(
state => state.counter
)
这里:
state
指:
Redux 的整个 state。
假设:
state = {
counter: {
count: 0
}
}
那么:
state.counter
得到:
{
count: 0
}
然后:
const { count } = ...
得到:
count = 0
所以:
useSelector用来从 Redux 中读取数据。
可以简单记:
useSelector = 读
二十、useDispatch:React 组件发送 action
首先:
const dispatch = useDispatch()
相当于:
从 Redux 拿到 dispatch 函数。
然后:
<button
onClick={() => dispatch(increment())}
>
+
</button>
点击以后:
increment()
↓
生成 action
{
type: 'counter/increment'
}
↓
dispatch 发送
↓
Redux 找到 reducer
↓
执行:
state.count++
于是 Redux 数据更新。
二十一、React + Redux 完整闭环
把:
useSelector
和:
useDispatch
放在一起,就形成完整数据流:
Redux store
↓
useSelector
↓
React拿到 count
↓
页面显示 count
↓
用户点击按钮
↓
dispatch(increment())
↓
Redux reducer
↓
state.count++
↓
Redux state更新
↓
useSelector得到新值
↓
React重新渲染
这才是 React + Redux 最完整的一条线。
二十二、action 怎么传参数
前面的:
increment()
不需要参数,因为它永远:
+1
但如果需求变成:
加 10、加 20、加任意数字
就需要传参数。
例如:
javascript
reducers: {
addToNum(state, action) {
state.count += action.payload
}
}
组件:
dispatch(addToNum(10))
Redux Toolkit 会让:
addToNum(10)
生成类似:
{
type: 'counter/addToNum',
payload: 10
}
所以:
action.payload
就是:
10
最后:
state.count += action.payload
相当于:
state.count += 10
二十三、payload 到底是什么
可以直接记:
type
=
我要做什么
payload
=
做这件事情需要的数据
例如:
{
type: 'counter/addToNum',
payload: 20
}
翻译成人话:
我要执行 addToNum,这次带的数据是 20。
所以:
dispatch(addToNum(20))
完整流程:
addToNum(20)
↓
生成:
{
type: 'counter/addToNum',
payload: 20
}
↓
dispatch
↓
reducer
↓
action.payload
↓
20
↓
state.count += 20
二十四、Redux 异步请求为什么又多一层
同步修改:
dispatch(increment())
马上就可以计算。
但是请求接口不一样。
比如:
await axios.get(url)
需要等待服务器返回。
所以异步 Redux 一般采用:
异步函数负责请求数据
同步 reducer 负责把数据存进 store
这是异步 Redux 最核心的思想。
二十五、频道列表异步请求示例
先创建频道模块:
javascript
import { createSlice } from '@reduxjs/toolkit'
import axios from 'axios'
const channelStore = createSlice({
name: 'channel',
initialState: {
channelList: []
},
reducers: {
setChannels(state, action) {
state.channelList = action.payload
}
}
})
这里:
setChannels(state, action)
只负责一件事:
把频道数据存到 Redux。
例如:
javascript
action.payload = [
{ id: 0, name: '推荐' },
{ id: 1, name: 'HTML' }
]
那么:
state.channelList = action.payload
完成数据保存。
二十六、异步函数本身不直接修改 state
接下来:
javascript
const { setChannels } = channelStore.actions
const fetchChannelList = () => {
return async (dispatch) => {
const res = await axios.get(
'http://geek.itheima.net/v1_0/channels'
)
dispatch(
setChannels(res.data.data.channels)
)
}
}
重点是:
fetchChannelList
负责:
请求接口
而:
setChannels
负责:
修改 Redux state
所以不要把它们混在一起。
可以把它们理解为:
fetchChannelList
= 去拿货
setChannels
= 把货放进仓库
二十七、为什么异步函数 return 另一个函数
const fetchChannelList = () => {
return async (dispatch) => {
}
}
调用:
fetchChannelList()
得到的不是普通 action 对象,而是:
async (dispatch) => {
...
}
也就是一个函数。
Redux Toolkit 默认支持 thunk,因此:
dispatch(fetchChannelList())
可以 dispatch 一个函数。
这个函数会拿到:
dispatch
于是可以:
const res = await axios.get(url)
dispatch(setChannels(...))
也就是:
先异步请求
↓
请求完成
↓
再 dispatch 同步 action
↓
reducer 保存数据
二十八、异步 Redux 的完整流程
组件:
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchChannelList())
}, [dispatch])
执行:
组件加载
↓
useEffect
↓
dispatch(fetchChannelList())
↓
fetchChannelList 返回异步函数
↓
Redux thunk 执行这个函数
↓
axios.get()
↓
接口返回频道列表
↓
dispatch(setChannels(频道数据))
↓
生成同步 action
↓
setChannels reducer
↓
state.channelList = action.payload
↓
Redux 更新
↓
useSelector 获取新数据
↓
组件重新渲染
所以异步 Redux 可以浓缩为:
先请求,再 dispatch;异步函数负责拿数据,同步 reducer 负责存数据。
二十九、同步和异步 Redux 对比
同步:
dispatch(increment())
流程:
action
↓
reducer
↓
state
异步:
dispatch(fetchChannelList())
流程:
异步函数
↓
请求服务器
↓
拿到数据
↓
dispatch(setChannels(data))
↓
action
↓
reducer
↓
state
所以异步只是在同步 Redux 前面又多了一步:
先把数据拿回来
最终修改 store,还是靠同步 reducer。
三十、完整 counterStore 示例
import { createSlice } from '@reduxjs/toolkit'
const counterStore = createSlice({
name: 'counter',
initialState: {
count: 0
},
reducers: {
increment(state) {
state.count++
},
decrement(state) {
state.count--
},
addToNum(state, action) {
state.count += action.payload
}
}
})
export const {
increment,
decrement,
addToNum
} = counterStore.actions
export default counterStore.reducer
三十一、完整 store 配置
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './modules/counterStore'
import channelReducer from './modules/channelStore'
const store = configureStore({
reducer: {
counter: counterReducer,
channel: channelReducer
}
})
export default store
最终 state:
{
counter: {
count: 0
},
channel: {
channelList: []
}
}
三十二、组件完整使用示例
import {
useDispatch,
useSelector
} from 'react-redux'
import {
increment,
decrement,
addToNum
} from './store/modules/counterStore'
function App() {
const { count } = useSelector(
state => state.counter
)
const dispatch = useDispatch()
return (
<div>
<button
onClick={() => dispatch(decrement())}
>
-
</button>
<span>{count}</span>
<button
onClick={() => dispatch(increment())}
>
+
</button>
<button
onClick={() => dispatch(addToNum(10))}
>
+10
</button>
</div>
)
}
export default App
三十三、最后真正需要记住的 Redux 数据流
如果前面全忘了,只要记住下面这条:
React组件
↓
dispatch(action)
↓
Redux收到action
↓
reducer根据 action 修改 state
↓
store保存新的state
↓
useSelector拿到新state
↓
React组件重新渲染
使用 Redux Toolkit 后:
createSlice
↓
帮你生成:
action creator
+
reducer
其中:
action creator
负责说:"我要做什么"
reducer
负责真正修改 state
例如:
dispatch(increment())
不是 increment() 自己让 count 加一。
而是:
increment()
↓
生成"我要加一"的 action
↓
dispatch
↓
Redux 找到 reducer
↓
increment(state)
↓
state.count++
再比如:
dispatch(addToNum(10))
就是:
addToNum(10)
↓
生成:
{
type: 'counter/addToNum',
payload: 10
}
↓
dispatch
↓
reducer
↓
action.payload = 10
↓
state.count += 10
如果是异步:
dispatch(fetchChannelList())
↓
请求接口
↓
拿到数据
↓
dispatch(setChannels(data))
↓
reducer
↓
数据存入 store
这就是 Redux 从原理到 Redux Toolkit、React-Redux,再到异步请求的完整知识链