文章目录
State
案例:计数器
import React, { useState } from 'react';
import './style.css';
export default function App() {
return (
<div>
<Counter />
</div>
);
}
function Counter() {
// State:计数器值
const [couter, setCounter] = useState(0);
// Action:发生某些事情时导致状态更新的代码
const increment = () => {
setCounter((prevCount) => prevCount + 1);
};
// View:定义UI
return (
<div>
Value: {couter} <button onClick={increment}>Increment</button>
</div>
);
}
上面代码具有以下部分:
- state:驱动应用程序的来源
- view:基于当前状态的UI声明性描述
- actions:基于用户交互在应用中发生的事件,并触发状态更新
这个例子是一个单向数据流:
- state 描述了应用程序在特定时间点的状况;
- 基于 state 来渲染UI;
- 执行事件时, state 会根据发生的事情进行更新;
- 基于新的 state 重新渲染UI
当有多个组件需要共享和使用相同state时,情况会变得复杂。可以通过提升 state 到父组件来解决这个问题。
也可以通过**从组件中提取共享 state ** 来解决,并将其放入组件树之外的一个集中位置。这样使得组件树成为一个大的”view“,任何组件都可以访问state或出发action,无论在什么位置。
通过定义和分离 state 管理中涉及的概念并强制执行维护 view 和 state 之间独立性的规则,代码变得更结构化和易于维护。
这就是 Redux 背后的基本思想:应用中使用集中式的全局状态来管理,并明确更新状态的模式,以便让代码具有可预测性。
不可变性 Immutablility
JavaScript 的对象(object)和数组(array)默认都是 mutable(可变)的,即可以任意更改内容。
const obj = { a: 1, b: 2 }
// 引用不变,内容变化
obj.b = 3
const arr = ['a', 'b']
// 引用不变,内容变化
arr.push('c')
arr[1] = 'd'
如果想要不可变的方式来更新,代码必需先 复制 原来的 object/array,然后更新它的复制体。
js的展开运算符可以实现这个目的:
const obj = {
a: {
// 为了安全的更新 obj.a.c,需要先复制一份
c: 3
},
b: 2
}
const obj2 = {
// obj 的备份
...obj,
// 覆盖 a
a: {
// obj.a 的备份
...obj.a,
// 覆盖 c
c: 42
}
}
const arr = ['a', 'b']
// 创建 arr 的备份,并把 c 拼接到最后。
const arr2 = arr.concat('c')
// 或者,可以对原来的数组创建复制体
const arr3 = arr.slice()
// 修改复制体
arr3.push('c')
Redux 期望所有状态更新都是使用不可变的方式。
Actions
action 是一个具有type
字段的普通 JavaScript 对象。可以将其视为描述应用程序中发生了什么事件。
type
字段是一个字符串,给这个 action 一个描述性的名字,比如"todos/todoAdded"
。通常将其写成**“域/事件名称”** :
- action 所属的特征或类别
- 发生的具体事情
action 对象具有其他字段,其中包含有关事件的附加信息,通常将其放在payload
字段中。
const addTodoAction = {
type: 'todos/todoAdded',
payload: 'Buy milk'
}
Reducers
reducer 是一个函数,接收当前的state
和一个 action
对象,决定如何更新状态,并返回新的状态。
(state, action) => newState
可以将 reducer 视为一个事件监听器,根据接收到的action类型处理事件。
Reducer 规则:
- 仅使用
state
和action
参数计算新的状态值 - 禁止直接修改
state
。必须通过复制现有的state
并对复制的值进行更改的方式来做 不可变更新(immutable updates)。 - 禁止任何异步逻辑、依赖随机值或导致其他“副作用”的代码
Reducer函数内部逻辑步骤:
- 检查 reducer 是否关心这个 action
- 如果是,则复制 state,使用新值更新 state 副本,然后返回新 state;
- 否则,返回原来的state不变。
例子:
const initialState = { value: 0 };
function conterReducer(state = initialState, action) {
// 检查 reducer 是否关心这个 action
if (action.type === 'conter/increment') {
// 如果是,复制 state
return {
...state,
// 使用新值更新 state 副本
value: state.value + 1,
};
}
// 返回原来的 state 不变
return state;
}
Store
当前 Redux 应用的状态存在于 store 对象中。
store 由通过传入一个 reducer 来创建,且有一个名为getState
的方法,返回当前的状态值;
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({ reducer: counterReducer })
console.log(store.getState())
// {value: 0}
Store 内部
store 实现的简化示例
function createStore(reducer, preloadedState) {
let state = preloadedState
const listeners = []
function getState() {
return state
}
function subscribe(listener) {
listeners.push(listener)
return function unsubscribe() {
const index = listeners.indexOf(listener)
listeners.splice(index, 1)
}
}
function dispatch(action) {
state = reducer(state, action)
listeners.forEach(listener => listener())
}
dispatch({ type: '@@redux/INIT' })
return { dispatch, subscribe, getState }
}
Redux store 汇集了构成应用程序的 state、actions 和 reducers。
store 有以下几个职责:
- 在内部保存当前应用程序 state
- 通过
store.getState()
访问当前 state; - 通过
store.dispatch(action)
更新状态; - 通过
store.subscribe(listener)
注册监听器回调; - 通过
store.subscribe(listener)
返回的unsubscribe
函数注销监听器。
注意 Redux 应用程序中只有一个 store。
Dispatch
Redux store 有一个dispatch
方法。
更新 state 的唯一方法是调用 store.dispatch()
并传入一个 action 对象。
store 将执行所有 reducer 函数并计算出更新后的 state,调用 getState()
可以获取新 state。
store.dispatch({ type: 'counter/incremented' })
console.log(store.getState())
// {value: 1}
dispatch 一个 action 即"触发一个事件"。
发生了一些事情,我们希望 store 知道这件事。 Reducer 就像事件监听器一样,当它们收到关注的 action 后,它就会更新 state 作为响应。
Selectors
Selector 函数可以从 store 状态树中提取指定的片段。随着应用变得越来越大,会遇到应用程序的不同部分需要读取相同的数据,selector 可以避免重复这样的读取逻辑:
const selectCounterValue = state => state.value
const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2
案例:TodoList应用
一、需求
- UI 应包括三个主要部分:
- 一个输入框,让用户输入新待办事项的文本
- 所有现有待办事项的列表
- 页脚部分,显示未完成的待办事项数量,并显示过滤选项
- 待办事项列表项应该有一个复选框来切换“完成”状态。还应该能够为预定义的颜色列表添加颜色编码的类别标签,并删除待办事项。
- 计数器应该复数活动待办事项的数量:“0 items”、“1 items”、“3 items”等
- 应该有按钮将所有待办事项标记为已完成,并通过删除它们来清除所有已完成的待办事项
- 应该有两种方法可以过滤列表中显示的待办事项:
- 待办事项可基于 All 、 Active 和 Completed 进行过滤
- 基于选择一种或多种颜色进行过滤,并显示标签与这些颜色匹配的任何待办事项
二、设计State
React 和 Redux 的核心原则之一是 UI 应该基于 state。
应用功能:
- 当前待办事项的实际列表
- 当前的过滤选项
对于每个待办事项所需存储的信息:
- 用户输入的文本
- 是否完成(布尔值)
- 唯一的ID
- 颜色类别(如果已选)
对于过滤行为:
- 状态:
“全部(All)”、“活动(Active)” 和 “已完成(Completed)”
- 颜色:
” Red “、“ Yellow ”、“ Green ”、“ Blue ”、“ Orange ”、“ Purple ”
State结构
待办事项数组:[
{
id: 唯一编号,
text: 用户输入文本,
completed:true|false 是否完成,
color: 可选的颜色类别,
}
]
过滤选项:{
已完成:,
颜色类别:[],
}
const todoAppState = {
todos: [
{ id: 0, text: 'Learn React', completed: true },
{ id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
{ id: 2, text: 'Build something fun!', completed: false, color: 'blue' },
],
filters: {
status: 'Active',
colors: ['red', 'blue'],
},
};
三、设计Actions
操作事件:
- 根据用户输入的文本添加新的待办事项条目
- 切换待办事项的完成状态
- 为待办事项选择颜色类别
- 删除待办事项
- 将所有待办事项标记为已完成
- 清除所有已完成的待办事项
- 选择不同的 “已完成” 过滤器值
- 添加新的滤色器
- 移除滤色器
基于以上操作事件构建actions列表:
{type: 'todos/todoAdded', payload: todoText}
{type: 'todos/todoToggled', payload: todoId}
{type: 'todos/colorSelected, payload: {todoId, color}}
{type: 'todos/todoDeleted', payload: todoId}
{type: 'todos/allCompleted'}
{type: 'todos/completedCleared'}
{type: 'filters/statusFilterChanged', payload: filterValue}
{type: 'filters/colorFilterChanged', payload: {color, changeType}}
四、编写Reducers
创建根Reducer
Redux 应用程序实际上只有一个 reducer 函数: 将“ root reducer ”传递给 createStore
函数。那个根 reducer 函数负责处理所有被 dispatching 的 actions,并计算每次所有的新 state 结果。
-
首先在
src
下创建reducer.js
文件,创建模拟数据并编写逻辑:// 模拟数据 const initialState = { todos: [ { id: 0, text: 'Learn React', completed: true }, { id: 1, text: 'Learn Redux', completed: false, color: 'purple' }, { id: 2, text: 'Build something fun!', completed: false, color: 'blue' }, ], filters: { status: 'Active', colors: ['red', 'blue'], }, }; // 使用 initialState 作为默认值 export default function appReducer(state = initialState, action) { // reducer 通常会根据 action type 字段来决定发生什么 switch (action.type) { // 根据不同 type 的 action 在这里做一些事情 default: // 如果这个 reducer 不关心这个 action type,会返回原本的state return state; } }
-
添加处理
'todos/todoAdded' action
的逻辑:- 根据当前 action 类型与字符串进行匹配
- 返回包含状态的新对象
// 模拟数据 const initialState = { todos: [ { id: 0, text: 'Learn React', completed: true }, { id: 1, text: 'Learn Redux', completed: false, color: 'purple' }, { id: 2, text: 'Build something fun!', completed: false, color: 'blue' }, ], filters: { status: 'Active', colors: ['red', 'blue'], }, }; function nextTodoId(todos) { const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) return maxId + 1 } // 使用 initialState 作为默认值 export default function appReducer(state = initialState, action) { // reducer 通常会根据 action type 字段来决定发生什么 switch (action.type) { // 根据不同 type 的 action 在这里做一些事情 case 'todos/todoAdded': { // 需要返回一个新的 state 对象 return { // 具有所有现有 state 数据 ...state, // 但有一个用于 `todos` 字段的新数组 todos: [ // 所有旧待办事项 ...state.todos, // 新的对象 { // 在此示例中使用自动递增的数字 ID id: nextTodoId(state.todos), text: action.payload, completed: false, }, ], }; } default: // 如果这个 reducer 不关心这个 action type,会返回原本的state return state; } }
-
处理其他Actions:
// 模拟数据 const initialState = { todos: [ { id: 0, text: 'Learn React', completed: true }, { id: 1, text: 'Learn Redux', completed: false, color: 'purple' }, { id: 2, text: 'Build something fun!', completed: false, color: 'blue' }, ], filters: { status: 'Active', colors: ['red', 'blue'], }, }; function nextTodoId(todos) { const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) return maxId + 1 } export default function appReducer(state = initialState, action) { switch (action.type) { // 添加待办事项 case 'todos/todoAdded': { return { ...state, todos: [ ...state.todos, { id: nextTodoId(state.todos), text: action.payload, completed: false, }, ], }; } // 切换完成状态 case 'todos/todoToggled': { return { ...state, todos: state.todos.map((todo) => { if (todo.id !== action.payload) { return todo; } return { ...todo, completed: !todo.completed, }; }), }; } // 修改过滤器 case 'filters/statusFilterChanged': { return { ...state, filters: { ...state.filters, status: action.payload, }, }; } default: return state; } }
由于代码过长导致可读性很差,所以应该将reducer拆分成多个较小的reducer函数。
拆分Reducers
作为其中的一部分,Redux reducer 通常根据更新的 Redux state 部分进行拆分。我们的 todo 应用 state 当前有两个顶级部分:state.todos
和 state.filters
。因此,可以将大的根 reducer 函数拆分为两个较小的 reducer - todosReducer
和 filtersReducer
。
-
在src下新建
features
文件夹,创建todos
文件夹,新建todosSlice.js
文件,写入与todo相关的代码:const initialState = [ { id: 0, text: 'Learn React', completed: true }, { id: 1, text: 'Learn Redux', completed: false, color: 'purple' }, { id: 2, text: 'Build something fun!', completed: false, color: 'blue' }, ]; function nextTodoId(todos) { const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1); return maxId + 1; } export default function todosReducer(state = initialState, action) { switch (action.type) { default: return state; } }
写入与todo有关的操作:
const initialState = [ { id: 0, text: 'Learn React', completed: true }, { id: 1, text: 'Learn Redux', completed: false, color: 'purple' }, { id: 2, text: 'Build something fun!', completed: false, color: 'blue' }, ]; function nextTodoId(todos) { const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1); return maxId + 1; } export default function todosReducer(state = initialState, action) { switch (action.type) { case 'todos/todoAdded': { return [ ...state, { id: nextTodoId(state), text: action.payload, completed: false, }, ]; } case 'todos/todoToggled': { return state.map((todo) => { if (todo.id !== action.payload) { return todo; } return { ...todo, completed: !todo.completed, }; }); } default: return state; } }
-
创建
src/features/filters/filtersSlices.js
,写入与过滤器相关代码const initialState = { status: 'All', colors: [], }; export default function filtersReducer(state = initialState, action) { switch (action.type) { case 'filters/statusFilterChanged': { return { ...state, status: action.payload, }; } default: return state; } }
-
完整的reducer实现:
todosSlice.jsconst initialState = [ { id: 0, text: 'Learn React', completed: true }, { id: 1, text: 'Learn Redux', completed: false, color: 'purple' }, { id: 2, text: 'Build something fun!', completed: false, color: 'blue' }, ]; function nextTodoId(todos) { const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1); return maxId + 1; } export default function todosReducer(state = initialState, action) { switch (action.type) { // 根据用户输入的文本添加新的待办事项条目 case 'todos/todoAdded': { return [ ...state, { id: nextTodoId(state), text: action.payload, completed: false, }, ]; } // 切换待办事项的完成状态 case 'todos/todoToggled': { return state.map((todo) => { if (todo.id !== action.payload) { return todo; } return { ...todo, completed: !todo.completed, }; }); } // 为待办事项选择颜色类别 case 'todos/colorSelected': { const [color, todoId] = action.payload; return state.map((todo) => { if (todo.id != todoId) { return todo; } return { ...todo, color, }; }); } // 删除待办事项 case 'todos/todoDeleted': { return state.filter((todo) => todo.id !== action.payload); } // 将所有待办事项标记为已完成 case 'todos/allCompleted': { return state.map((todo) => { return { ...todo, completed: true, }; }); } // 清除所有已完成的待办事项 case 'todo/completedCleared': { return state.filter((todo) => !todo.completed); } default: return state; } }
filtersSlice.js
const initialState = { status: 'All', colors: [], }; export default function filtersReducer(state = initialState, action) { switch (action.type) { // 选择不同的 “已完成” 过滤器值 case 'filters/statusFilterChanged': { return { ...state, status: action.payload, }; } // 修改滤色器 case 'filters/colorFilterChanged': { let { color, changeType } = action.payload; const { colors } = state; switch (changeType) { // 添加新的滤色器 case 'added': { if (colors.includes(color)) { return state; } return { ...state, colors: state.colors.concat(color), }; } // 移除滤色器 case 'removed': { return { ...state, colors: state.colors.filter( (existingColor) => existingColor !== color ), }; } default: return state; } } default: return state; } }
组合 Reducers
存储在创建时需要 一个 根 reducer 函数。那么,如何才能在不将所有代码放在一个大函数中的情况下重新使用根 redux 呢?
由于 reducers 是一个普通的 JS 函数,我们可以将 slice reducer 重新导入 reducer.js
,并编写一个新的根 reducer,它唯一的工作就是调用其他两个函数。
import todosReducer from './features/todos/todosSlice';
import filtersReducer from './features/filters/filtersSlice';
export default function rootReducer(state = {}, action) {
// 返回一个新的根 state 对象
return {
// `state.todos` 的值是 todos reducer 返回的值
todos: todosReducer(state.todos, action),
// 对于这两个reducer,我们只传入它们的状态 slice
filters: filtersReducer(state.filters, action),
};
}
这些 reducer 中的每一个都在管理自己的全局状态部分。每个 reducer 的 state 参数都是不同的,并且对应于它管理的 state 部分。
combineReducers
Redux 核心库包含一个名为 combineReducers
的实用程序,它为我们执行相同的样板步骤。我们可以用 combineReducers
生成的较短的 rootReducer
替换手写的 rootReducer
。
npm i redux
import { combineReducers } from 'redux';
import todosReducer from './features/todos/todosSlice';
import filtersReducer from './features/filters/filtersSlice';
const rootReducer = combineReducers({
todos: todosReducer,
filters: filtersReducer,
});
export default rootReducer;
五、创建store
Redux 核心库有一个 createStore
API 可以创建 store。新建一个名为 store.js
的文件,并导入 createStore
和根 reducer。然后,调用 createStore
并传入根 reducer :
import { createStore } from 'redux';
import rootReducer from './reducer';
const store = createStore(rootReducer);
export default store;
加载初始 State
createStore
的第二个参数为preloadedState
,可以用于添加初始数据。例如包含在从服务器接收到的 HTML 页面中的值,或保存在 localStorage
中并在用户再次访问该页面时读回的值,如下所示:
import { createStore } from 'redux'
import rootReducer from './reducer'
let preloadedState
const persistedTodosString = localStorage.getItem('todos')
if (persistedTodosString) {
preloadedState = {
todos: JSON.parse(persistedTodosString)
}
}
const store = createStore(rootReducer, preloadedState)
Dispatching Actions
对代码进行测试:
index.js
import React, { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import store from './store';
// 打印 Initial state
console.log('Initial state: ', store.getState());
// {todos: [....], filters: {status, colors}}
// 每次状态变化时,记录一下
// 请注意,subscribe() 返回一个用于解绑侦听器的函数
const unsubscribe = store.subscribe(() =>
console.log('State after dispatch: ', store.getState())
);
// dispatch 一些 actions
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' });
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about reducers' });
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about stores' });
store.dispatch({ type: 'todos/todoToggled', payload: 0 });
store.dispatch({ type: 'todos/todoToggled', payload: 1 });
store.dispatch({ type: 'filters/statusFilterChanged', payload: 'Active' });
store.dispatch({
type: 'filters/colorFilterChanged',
payload: { color: 'red', changeType: 'added' },
});
// 停止监听 state 的更新
unsubscribe();
// 再 dispatch 一个 action,看看发生了什么
store.dispatch({ type: 'todos/todoAdded', payload: 'Try creating a store' });
const rootElement = document.getElementById('root');
const root = createRoot(rootElement);
root.render(
<StrictMode>
<App />
</StrictMode>
);