Redux 基本概念与 API
Store 就是保存数据的地方,你可以把它看成一个容器。整个应用只能有一个 Store。
Store
对象包含所有数据。如果想得到某个时点的数据,就要对 Store 生成快照。这种时点的数据集合,就叫做 State。
State 的变化,会导致 View 的变化。但是,用户接触不到 State,只能接触到 View。所以,State 的变化必须是 View 导致的。Action 就是 View 发出的通知,表示 State 应该要发生变化了。
Action 是一个对象。其中的type
属性是必须的,表示 Action 的名称。
Action 描述当前发生的事情。改变 State 的唯一办法,就是使用 Action。它会运送数据到 Store。
Reducer 是一个函数,它接受 当前 State 和 Action 作为参数,返回一个新的 State。
Store 允许使用store.subscribe
方法设置监听函数,一旦 State 发生变化,就自动执行这个函数。
store 的三个方法
- store.getState() // 获取当前 state
- store.dispatch() // 派发 action 至 reducer 处理
- store.subscribe() // 订阅事件 state 变化即触发
示例代码
实现一个能够添加书籍,并能够根据书籍 ID 删除的功能。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Redux example</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div id="app">
<div id="addBook">
<input id="addInput" type="text" />
<button id="addBtn">ADD Book</button>
</div>
<div id="delBook">
<input id="delInput" type="text" />
<button id="delBtn">DEL Book</button>
</div>
<ol id="bookList"></ol>
</div>
<script src="app.js"></script>
</body>
</html>
import { legacy_createStore as createStore } from "redux";
const initialState = [];
let recordState;
const reducer = (state = initialState, action) => {
recordState = state;
switch (action.type) {
case "ADD_BOOK":
return [
...state,
{
bookId: action.info.bookId,
bookName: action.info.bookName,
},
];
case "DEL_BOOK":
return state.filter((book) => book.bookId != action.info.bookId);
default:
return [...state];
}
};
const store = createStore(reducer);
const addInput = document.getElementById("addInput");
const delInput = document.getElementById("delInput");
const addBtn = document.getElementById("addBtn");
const delBtn = document.getElementById("delBtn");
const bookList = document.getElementById("bookList");
function* generateId() {
let i = 0;
while (true) {
yield ++i;
}
}
const generateID = generateId();
const bookId = () => generateID.next().value;
function addBookFn() {
const bookName = addInput.value;
if (bookName.trim()) {
addInput.value = "";
const id = bookId();
store.dispatch({
type: "ADD_BOOK",
info: {
bookName,
bookId: id,
},
});
}
}
function delBookFn() {
const bookId = delInput.value.trim();
const books = store.getState();
const isExist = books.find((book) => book.bookId == bookId);
if (isExist) {
delInput.value = "";
store.dispatch({
type: "DEL_BOOK",
info: {
bookId,
},
});
}
}
addBtn.addEventListener("click", addBookFn);
delBtn.addEventListener("click", delBookFn);
function createBookItem(book) {
const element = document.createElement("li");
element.innerText = `BookID: ${book.bookId} Name: ${book.bookName}`;
return element;
}
const showBookList = store.subscribe(() => {
const newBookList = store.getState();
if (newBookList.length !== recordState.length) {
bookList.innerText = "";
newBookList.forEach((book) => {
bookList.appendChild(createBookItem(book));
});
}
});
直接使用 Redux 存在的常见问题
- 配置复杂,devtool…
- 模板代码太多,创建 constant,action,reducer…
- 需要添加很多依赖包,如 redux-thunk、immer…
Redux-toolkit 解决了哪些问题
configureStore()
包裹 createStore,并集成了redux-thunk
、Redux DevTools Extension
,默认开启
createReducer()
创建一个 reducer,action type 映射到 case reducer 函数中,不用写 switch-case,并集成`immer
createAction()
创建一个 action,传入动作类型字符串,返回动作函数
createSlice()
创建一个 slice,包含 createReducer、createAction 的所有功能
createAsyncThunk()
创建一个 thunk,接受一个动作类型字符串和一个 Promise 的函数
Redux-toolkit 替换 Redex 示例代码
安装@reduxjs/toolkit
# 使用 npm npm install @reduxjs/toolkit # 使用 yarn yarn add @reduxjs/toolkit
configureStore 替换 createStore
import React from "react"; import { render } from "react-dom"; // import { createStore } from "redux"; import { configureStore } from "@reduxjs/toolkit"; import { Provider } from "react-redux"; import App from "./components/App"; import rootReducer from "./reducers"; // const store = createStore(rootReducer); const store = configureStore({ reducer: rootReducer, });
创建 Action
# 创建 action const increment = createAction('INCREMENT') const decrement = createAction('DECREMENT') # 创建reducer const counter = createReducer(0, { [increment]: state => state + 1, [decrement]: state => state - 1 })
以上看起比原来结构上好一些,创建 action、reducer 方便了,但是看着还是不爽,action 也可以去掉。
创建 Slice
const counterSlice = createSlice({ name: 'counter', initialState: 0, reducers: { increment: state => state + 1, decrement: state => state - 1 } }) # action counterSlice.action; # reducer counterSlice.reducer;
完整 slice
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { AppThunk, RootState } from "../../app/store"; interface CounterState { value: number; } const initialState: CounterState = { value: 0, }; export const counterSlice = createSlice({ name: "counter", initialState, reducers: { increment: (state) => { state.value += 1; }, decrement: (state) => { state.value -= 1; }, incrementByAmount: (state, action: PayloadAction<number>) => { state.value += action.payload; }, }, }); export const { increment, decrement, incrementByAmount } = counterSlice.actions; export const incrementAsync = (amount: number): AppThunk => (dispatch) => { setTimeout(() => { dispatch(incrementByAmount(amount)); }, 1000); }; export const selectCount = (state: RootState) => state.counter.value; export default counterSlice.reducer;
import React, { useState } 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() { const count = useSelector(selectCount); const dispatch = useDispatch(); const [incrementAmount, setIncrementAmount] = useState("2"); 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> </div> <div className={styles.row}> <input className={styles.textbox} aria-label="Set increment amount" value={incrementAmount} onChange={(e) => setIncrementAmount(e.target.value)} /> <button className={styles.button} onClick={() => dispatch(incrementByAmount(Number(incrementAmount) || 0)) } > Add Amount </button> <button className={styles.asyncButton} onClick={() => dispatch(incrementAsync(Number(incrementAmount) || 0)) } > Add Async </button> </div> </div> ); }