学习了 React 就会提到 Redux / React Redux / Redux Toolkit 这些东西?它们到底是什么呢?又有什么关系呢?带着这些问题,我们一起开始学习吧。
PS: 本文尽量只使用核心代码解释 Redux、 React-Redux 、Redux/Toolkit 的用法,梳理核心用法和概念。而不是琳琅满目的概念和用法,让学习的人害怕。
不可变性
先看一段特别简单的 js 代码:
js
const Bob = {
firstName: "Bob",
lastName: "Loblaw",
}
function giveAwesomePowers(person) {
person.specialPower = "invisibility";
return person;
}
giveAwesomePowers(Bob)
console.log(Bob.specialPower);
我们知道 giveAwesomePowers 函数会修改 Bob 的属性,一个变量被传给一个函数竟然修改了自己的属性,这会引起代码混乱,破坏了 纯函数 的概念,函数应该像数学公式一样,传入了一个参数,返回一个结果。不应该产生其他影响(副作用),不应该影响(修改了)全局变量 Bob。
这就是 不可变性,怎么优化上面的函数呢?如下:
js
function giveAwesomePowers(person) {
let newPerson = Object.assign({}, person, {
specialPower: 'invisibility'
})
return newPerson;
}
这样就保持了 不可变性 ,一个函数的执行,并没有对其他变量造成"意料之外"的影响。为什么要讲不可变性呢?因为 Redux 的原则就是 不可变性,函数对传入的数据不直接进行修改,需要复制一份,然后修改复制的数据,并返回复制的数据。
# Immutability in React and Redux: The Complete Guide
# A Visual Guide to References in JavaScript
Redux
html
<!DOCTYPE html>
<html>
<head>
<title>Redux basic example</title>
</head>
<body>
<div>
<p>
Clicked: <span id="value">0</span> times
<button id="increment">+</button>
<button id="decrement">-</button>
<button id="incrementIfOdd">Increment if odd</button>
<button id="incrementAsync">Increment async</button>
</p>
</div>
<script type="module">
import { createStore } from "https://unpkg.com/redux@latest/dist/redux.browser.mjs";
const initialState = {
value: 0
};
function counterReducer(state = initialState, action) {
switch (action.type) {
case "counter/incremented":
return { ...state, value: state.value + 1 };
case "counter/decremented":
return { ...state, value: state.value - 1 };
default:
return state;
}
}
const store = createStore(counterReducer);
const valueEl = document.getElementById("value");
function render() {
const state = store.getState();
valueEl.innerHTML = state.value.toString();
}
render();
store.subscribe(render);
document
.getElementById("increment")
.addEventListener("click", function () {
store.dispatch({ type: "counter/incremented" });
});
document
.getElementById("decrement")
.addEventListener("click", function () {
store.dispatch({ type: "counter/decremented" });
});
document
.getElementById("incrementIfOdd")
.addEventListener("click", function () {
if (store.getState().value % 2 !== 0) {
store.dispatch({ type: "counter/incremented" });
}
});
document
.getElementById("incrementAsync")
.addEventListener("click", function () {
setTimeout(function () {
store.dispatch({ type: "counter/incremented" });
}, 1000);
});
</script>
</body>
</html>
可以看到 Redux 是一个独立的管理数据状态的库,并没有和 React 锁死的。最常见的用法是使用 React 和 React Native,但也有适用于 Angular、Angular 2、Vue、Mithril 等的绑定。Redux 只是提供了一种订阅机制,任何其他代码都可以使用它。
从上面基础用法我们可以看到 Redux 的一些使用的概念:
Store
当前 Redux 应用的 状态 存在于一个名为 store 的对象中。
store 是通过传入一个 reducer 来创建的,并且有一个名为 getState
的方法,它返回当前状态值:
js
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({ reducer: counterReducer })
console.log(store.getState())
// {value: 0}
State
通常将 Store 中存的数据状态成为 State, 可以通过 Store 的 getState
方法获取到。
Actions
action 是一个具有 type
字段的普通 JavaScript 对象。你可以将 action 视为描述应用程序中发生了什么的事件.
上面例子中的 { type: "counter/incremented" }
就是对应的 action 的概念。对 Store 里状态会进行不同的操作,对这些操作进行统一的命名和管理。
Reducers
reducer 是一个函数,接收当前的 state
和一个 action
对象,必要时决定如何更新状态,并返回新状态。函数签名是:(state, action) => newState
。 你可以将 reducer 视为一个事件监听器,它根据接收到的 action(事件)类型处理事件。
对应示例代码中 function counterReducer(state = initialState, action) {}
函数,根据 action 类型对 Store 中的状态数据 state 进行处理,并返回一个 newState。
Dispatch
Redux store 有一个方法叫 dispatch
。更新 state 的唯一方法是调用 store.dispatch()
并传入一个 action 对象 。 store 将执行所有 reducer 函数并计算出更新后的 state,调用 getState()
可以获取新 state。
React-Redux
初始化项目
上面是一个独立使用 Redux 的例子,例子中数据更新逻辑在 render()
函数中,以及按钮的操作都是我们自己写的,在 React 中,可以利用 React-Redux 库来帮我们完成数据更新的逻辑,以及和 React 和结合绑定。使用如下:
lua
npx create-react-app count-demo
npm install react-redux --save
npm install redux --save
action/reducer/store 代码都是使用的 Redux 的方法,没有什么好说的,我们看下组件 CountNum 中是怎么获取 Store 中的 state,以及组件 CountButton 怎么出发 action 修改 state。
CountNum
jsx
// CountNum
import React from 'react';
import { connect } from 'react-redux';
function CountNum(props) {
return (
<div>{props.count}</div>
)
}
const mapStateToProps = state => {
return state
}
export default connect(mapStateToProps)(CountNum)
CountButton
jsx
// CountButton.jsx
import React from 'react';
import { connect } from 'react-redux';
import { addAction, reduceAction } from '../react-redux/actions';
function CountButton(props) {
const addTen = ()=> {
props.sendAdd(10)
}
const reduceTwo =()=> {
props.sendReduce(2)
}
return (
<>
<button onClick={addTen}> +10</button>
<button onClick={reduceTwo}> -2 </button>
</>
)
}
const mapDispatchToProps = dispatch=> {
return {
sendAdd: (num)=> {
dispatch(addAction(num))
},
sendReduce: (num)=> {
dispatch(reduceAction(num))
}
}
}
export default connect(null, mapDispatchToProps)(CountButton)
connect
连接 React 组件和 Redux store 的关键函数是 connect
,语法如下:
jsx
import { connect } from 'react-redux';
const ConnectedComponent = connect(mapStateToProps, mapDispatchToProps)(YourComponent);
-
mapStateToProps(state, ownProps?)
: 这是一个函数,它接收整个store的状态state
作为第一个参数,可选地接收组件自身的propsownProps
作为第二个参数,然后返回一个对象,该对象中的每个键值对都会被合并到组件的props中。这样,你的组件就可以访问到store中的数据了。 -
mapDispatchToProps(dispatch, ownProps?)
: 这也是一个函数,但通常你会看到它被省略或以另一种形式定义。它的目的是将action creators绑定到dispatch方法,使得组件可以直接调用这些action creator函数来触发state的更新。你也可以直接传递一个对象,其中的函数会被自动dispatch。
不可变性
Redux 重要一点是 不可变性,在 reducer 中,并没有直接操作 state,而是重新返回了一个新的对象:
js
// reducer/index.js
const initialState = {
count: 0
}
export const reducer = (state = initialState, action)=> {
switch (action.type) {
case 'ADD_NUM':
return {
...state,
count: state.count + action.payload.num
}
case 'REDUCE_NUM':
return {
...state,
count: state.count - action.payload.num
}
default:
return state
}
}
# git 源码
# 最容易理解的react-redux 入门实战讲解
# React-Redux 入门讲解实战
Redux/Toolkit
Redux/Toolkit 最初发布于2019年。它是Redux团队官方推荐的一套工具集,旨在简化Redux的开发过程,减少样板代码,并提供最佳实践。随着Redux的发展,Redux/Toolkit逐渐成为了创建Redux应用的首选方法,特别是在Redux 4.2.0版本之后,其中的createStore
方法被标记为已弃用,进一步推动了向Redux/Toolkit及其configureStore
方法的过渡。来,我们看看 Redux/Toolkit 做了哪些简化,以及怎么使用的呢。
初始化项目
lua
# Redux + Plain JS template
npx create-react-app my-app --template redux
# Redux + TypeScript template
npx create-react-app my-app --template redux-typescript
Redux/Toolkit 模板里面把逻辑都写好了,我们直接看关键代码,在入口文件 index.js 中,还是通过 Provider 和 store 通过根节点注入到项目中:
js
// src\index.js
import { store } from './app/store';
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
Store
我们看下 Store 是怎么初始化的:
js
// src\app\store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
@reduxjs/toolkit
中通过 configureStore
初始化 Store 的。
Slice
slice
是一个对象,有 actions
和 reducer
属性:
jsx
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { fetchCount } from './counterAPI';
const initialState = {
value: 0,
status: 'idle',
};
// 异步
export const incrementAsync = createAsyncThunk(
'counter/fetchCount',
async (amount) => {
const response = await fetchCount(amount);
return response.data;
}
);
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
// Redux Toolkit 内部集成了 Immer,可以直接修改参数 state
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
extraReducers: (builder) => {
builder
.addCase(incrementAsync.pending, (state) => {
state.status = 'loading';
})
.addCase(incrementAsync.fulfilled, (state, action) => {
state.status = 'idle';
state.value += action.payload;
});
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export const selectCount = (state) => state.counter.value;
export default counterSlice.reducer;
- 初始化 Store 时,可以传入
counterSlice.reducer
- 通过
selectCount
获取 Store 中的数据 - 通过
counterSlice.actions
返回 action - 对于异步请求,可以使用
createAsyncThunk
。
更多使用细节# Redux Toolkit: 概览
不可变性
Redux/Toolkit 内部使用了一个名为 Immer 的库。Immer 会跟踪所有更改,然后使用更改列表返回一个安全不可变更新的值,看起来就像手动编写了所有不可变更新逻辑一样。 所以不需要这样编写:
js
function handwrittenReducer(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
}
可以这样写:
js
function reducerWithImmer(state, action) {
state.first.second[action.someId].fourth = action.someValue
}