重学React:应用规模化之状态管理

开发 前端
我们通过TodoMVC​的例子掌握了React​的很多核心知识点,搞一个小应用不成问题,但是,但凡上点规模的应用都会需要状态管理和路由​。所以,我们将继续升级TodoMVC​,引入这两个关键需求,使大家可以通过这个过程掌握规模化React应用中如何用好状态管理和路由功能。

图片

前言

小羊们好!我们通过TodoMVC的例子掌握了React的很多核心知识点,搞一个小应用不成问题,但是,但凡上点规模的应用都会需要状态管理和路由。所以,我们将继续升级TodoMVC,引入这两个关键需求,使大家可以通过这个过程掌握规模化React应用中如何用好状态管理和路由功能。

我们将学到如下核心知识点:

  • 如何选择一个状态管理库
  • 如何在React中引入Redux
  • 如何编写模块化的Redux代码
  • 如何通过RTK避免模版代码
  • 如何编写Redux中间件
  • 理解不可变数据的思想
  • 进一步优化拆分组件结构
  • ...

常用状态库选择

现在react社区中的状态管理库有一打那么多,随便说几个:

  • redux、mobx、recoil、zustand、jotai、valtio、resso、...
  • 我说一下自己对它们的一些个人看法:
  • redux​:老牌劲旅,59k Star;使用复杂且臃肿,RTK的出现让redux焕发了青春。
  • mobx:中坚力量,26k Star;使用简单,响应式,但是 "不够 React"
  • recoil:贵族血统,18k Star;Meta推出,使用简单、原子化;但是处于试验状态
  • zustand:后起之秀,23.4k Star;小、快、灵,简单版redux
  • jotai:10.7k Star;元数据化,hooks写法,符合hooks理念
  • valtio:5.7k Star;proxy理念,"不太 React",用起来简单
  • resso:0.3K Star;号称世界上最简洁的状态管理库,适用于RN、SSR、小程序

看完这些之后,大家很容易做选择:

想要大众稳定一点,redux,mobx

  • 想要简单,后续新出这几个都属于这种风格
  • 想要时髦选recoil
  • 想要小巧选resso
  • 本次案例我将选用redux来做演示,它最具有代表性,设计理念也比较有学习价值。

之前案例问题分析

现在我们来看一下前面的案例中一些比较别扭的地方:

TodoList传递很多属性和方法比较冗长:

<TodoList {...{todos: filteredTodos, removeTodo, updateTodo}}></TodoList>

visibility,setVisibility明显是TodoFilter内部状态,现在因为过滤结果要传给TodoList而不得不写在外面:

<TodoFilter visibility={visibility} setVisibility={setVisibility}></TodoFilter>

如果提取新增功能到独立组件里也会遇到同样的问题:操作的是todos,关心的却是TodoList,很不好写吧?

图片

我们期待的​​App.jsx​​应该是这样的:简单组合,组件之间又可以轻松共享数据和通信。

<AddTodo></AddTodo>
<TodoList></TodoList>
<TodoFilter></TodoFilter>

在组件内部,也应该很容易取到想要的数据和方法,比如TodoList:

function TodoList() {
// 一个hook可以轻松获取数据
const todos = useSelector(state state.todos)
// 一个hook可以获取修改数据的方法
const dispatch = useDispatch()
// 需要修改数据时派发action即可
dispatch(deleteTodoAction)
}

像上面这样的需求,通过redux这样的同一状态管理库就可以很容易实现,下面我们来看一下具体做法。

引入Redux

以前我使用redux需要安装:redux + react-redux。痛点是概念多,代码复杂冗长,心智负担严重。

现在官方推出了Redux Toolkit,以下简称RTK,我们可以使用RTK + react-redux 组合。

RTK主要用来简化和优化redux代码。使用它可以轻松实现模块化和可变数据写法,简直不要太好用,这也是我改变最初准备使用mobx给大家演示的原因!

下面我们引入RTK 和 react-redux:

yarn add @reduxjs/toolkit react-redux

我想要快速尝试一下,store/index.js:创建Store实例,用来存储状态

import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
// configureStore()创建一个store实例
export const store = configureStore({
reducer: {
// counter即为模块名称
counter: counterReducer,
},
});

再看看counterSlice中的reducer定义部分:createSlice({...})定义子模块,这样可以很容易拆分代码

import { createSlice } from '@reduxjs/toolkit';

// createSlice定义子模块
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
// `reducers`就是我们用来修改状态的方法
reducers: {
increment: (state) => {
// Redux Toolkit 使我们可以直接修改状态,大幅减少模版代码
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
}
}
});
// 导出actionsCreator便于用户使用,例如:dispatch(increment())
export const { increment, decrement } = counterSlice.actions;
// 导出子模块reducer
export default counterSlice.reducer;

下面是在主文件中设置store,main.jsx:

import store from './store'
import { Provider } from 'react-redux'

ReactDOM.createRoot(document.getElementById('root')).render(
// Provider可以将store透传下去
<Provider store={store}>
<React.StrictMode>
<App />
</React.StrictMode>
</Provider>
)

这就准备好了,下面在组件中使用数据:

import { useSelector, useDispatch } from 'react-redux';
import { decrement, increment } from './store/counterSlice';

export function Counter() {
// useSelector(selector)获取需要的子模块数据
const count = useSelector(state state.counter.value);
// dispatch(action)用来修改状态
const dispatch = useDispatch();
return (
<div>
<button notallow={() => dispatch(decrement())}>-</button>
<span>{count}</span>
<button notallow={() => dispatch(increment())}>+</button>
</div>
)
}

看看效果吧!

图片

重构Todo应用

下面我们着手重构​​TodoMVC​​​,首先将todos数据和操作移入独立的​​todoSlice.js​​:

import { createSlice } from "@reduxjs/toolkit";

const initialState = {
value: JSON.parse(localStorage.getItem("todomvc-react") || "[]"),
};

// 创建todoSlice保存todos状态
// 将之前修改方法移至reducers中用来修改todos状态
const todoSlice = createSlice({
name: "todos",
initialState,
reducers: {
addTodo: ({ value: todos }, { payload: title }) => {
const id = todos[todos.length - 1] ? todos[todos.length - 1].id + 1 : 1
todos.push({
id,
title,
completed: false,
});
},
removeTodo: ({ value: todos }, { payload: id }) => {
const idx = todos.findIndex((todo) => todo.id === id);
todos.splice(idx, 1);
},
updateTodo: ({ value: todos }, { payload: editTodo }) => {
const todo = todos.find((todo) => todo.id === editTodo.id);
Object.assign(todo, editTodo);
},
},
});

// selector用于选出想要的数据
export const selectTodos = (state) => state.todos.value;
// actionCreator用于创建dispatch()需要的action
export const { addTodo, removeTodo, updateTodo } = todoSlice.actions;
export default todoSlice.reducer;

现在我们不再需要在app.jsx中声明todos,也不需要给TodoList传递数据:

function App() {
// const {todos, addTodo, removeTodo, updateTodo} = useTodos(todoStorage.fetch())
return (
<div className="App">
{/* ... */}
<TodoList ></TodoList>
{/* ... */}
</div>
);
}

todos发生变化持久化到localStorage可以先注释掉,随后我们通过中间件的方式写到todoSlice中

// useEffect(() => {
// todoStorage.save(todos);
// }, [todos]);

我们再重构一下TodoList.jsx:

import { useDispatch, useSelector } from "react-redux";
import { selectTodos, removeTodo, updateTodo } from "./store/todos";

const TodoList = () {
// 获取todos和dispatch
const todos = useSelector(selectTodos);
const dispatch = useDispatch();

const changeState = (e, currentTodo) => {
// currentTodo.completed = e.target.checked;
// 此处通过dispatch(updateTodo())方式通知更新todos状态
dispatch(updateTodo({ ...currentTodo, completed: e.target.checked }));
};

// ...
const onEditing = (e) => {
const title = e.target.value;
if (title) {
setEditedTodo({ ...editedTodo, title });
} else {
// removeTodo(editedTodo.id);
// 使用dispatch(removeTodo())删除指定项
dispatch(removeTodo(editedTodo.id));
}
};

const onEdited = (e) => {
if (e.code === "Enter") {
if (editedTodo.title) {
// updateTodo(editedTodo)
// 使用dispatch(updateTodo())更新
dispatch(updateTodo(editedTodo));
}
setEditedTodo(initial);
}
};
}

我们再提取新增组件感受一下变化,创建一个AddTodo.jsx:

import { useState } from "react";
import { useDispatch } from 'react-redux'
import { addTodo } from "./store/todoSlice";

export function AddTodo() {
// ...
// 这里只需要dispatch通知新增
const dispatch = useDispatch()
const onAddTodo = (e) => {
if (e.code === "Enter" && newTodo) {
// addTodo(newTodo)
// 修改新增待办调用方式
dispatch(addTodo(newTodo));
setNewTodo("");
}
};
// ...
}

下面修改过滤组件FilterTodo的实现:这里需要提取visibility这个状态到全局,因为修改组件是FilterTodo,关心它的却是TodoList,创建visibilitySlice.js:

import { createSlice } from "@reduxjs/toolkit";

export const VisibilityFilters = {
SHOW_ALL: "SHOW_ALL",
SHOW_COMPLETED: "SHOW_COMPLETED",
SHOW_ACTIVE: "SHOW_ACTIVE",
};

const visibilitySlice = createSlice({
name: "visibility",
initialState: VisibilityFilters.SHOW_ALL,
reducers: {
setVisibilityFilter(state, { payload }) {
return payload;
},
},
});

export const { setVisibilityFilter } = visibilitySlice.actions
export default visibilitySlice.reducer

注册slice,store/index.js

import visibilitySlice from './visibilitySlice'

export default configureStore({
reducer: {
visibility: visibilitySlice
}
})

修改FilterTodo.jsx:可以看到FilterTodo通过dispatch()方式设置visibility。

import { useDispatch, useSelector } from "react-redux";
import {
VisibilityFilters,
setVisibilityFilter,
} from "./store/visibilitySlice";

export default function TodoFilter() {
// 引入visibility
const visibility = useSelector(state state.visibility)
// 获取选中状态
const getSelectedClass = (filter) =>
visibility === filter ? "selected" : "";
// 引入dispatch
const dispatch = useDispatch();
// 设置过滤
const setFilter = (filter) => dispatch(setVisibilityFilter(filter));

return (
<ul className="filters">
<li>
<button
className={getSelectedClass(VisibilityFilters.SHOW_ALL)}
notallow={() => setFilter(VisibilityFilters.SHOW_ALL)}
>
All
</button>
</li>
<li>
<button
className={getSelectedClass(VisibilityFilters.SHOW_ACTIVE)}
notallow={() => setFilter(VisibilityFilters.SHOW_ACTIVE)}
>
Active
</button>
</li>
<li>
<button
className={getSelectedClass(VisibilityFilters.SHOW_COMPLETED)}
notallow={() => setFilter(VisibilityFilters.SHOW_COMPLETED)}
>
Completed
</button>
</li>
</ul>
);
}

现在App.jsx中不在需要visibility状态,也不需要传递他们给FilterTodo

// const {visibility, setVisibility, filteredTodos} = useFilter(todos)
<TodoFilter></TodoFilter>

下面我们看一下visibility发生变化之后,todoSlice中如何做出响应:我们添加一个selectFilteredTodos选择器

export const selectFilteredTodos = ({ visibility, todos }) => {
if (visibility === VisibilityFilters.SHOW_ALL) {
return todos.value;
} else if (visibility === VisibilityFilters.SHOW_ACTIVE) {
return todos.value.filter((todo) => todo.completed === false);
} else {
return todos.value.filter((todo) => todo.completed === true);
}
};

TodoList中只需要替换selectTodos为selectFilteredTodos即可:

const todos = useSelector(selectFilteredTodos);

大家看这像不像Vue中的计算属性,或者Vuex中的getters,那既然要像,就要彻底一些,因此最好引入缓存性,即:todos和visibility不变化就没有必要重新过滤。

这就需要用到RTK提供的createSelector方法,它的前身就是reselect,来看看具体用法:

import { createSelector } from "@reduxjs/toolkit";
// 代码真是相当舒适!
export const selectFilteredTodos = createSelector(
(state) => state.visibility, // 选出所需状态作为输入
(state) => state.todos.value,// 选出所需状态作为输入
(visibility, todos) => { // 接收输入并执行派生逻辑
switch (visibility) {
case VisibilityFilters.SHOW_ACTIVE:
return todos.filter((todo) => todo.completed === false);
case VisibilityFilters.SHOW_COMPLETED:
return todos.filter((todo) => todo.completed === true);
default:
return todos;
}
}
);

全部搞定!现在再看看App.jsx,已经短小到不能再精悍了!

function App() {
return (
<div className="App">
<header>
<h1>我的待办事项</h1>
<img src={reactLogo} className="logo" alt="logo" />
</header>
{/* 新增 */}
<AddTodo></AddTodo>
{/* 列表 */}
<TodoList></TodoList>
{/* 过滤 */}
<TodoFilter></TodoFilter>
</div>
);
}

export default App;

使用Redux中间件

还有最后一件事,就是todos信息变化之后要存入localStorage。

之前我们通过观察todos变化触发保存行为:这需要我们在App中额外引入todos,破坏了App短小精悍的感觉

import { useSelector } from 'react-redux'
function App() {
// 这需要我们在App中额外引入todos
const todos = useSelector(state state.todos)
useEffect(() {
storage.save(todos)
}, [todos])
}

实际上,我们可以利用redux中间件完成这个需求,store/index.js:

import {todoStorage} from '../utils/storage'

// 声明一个中间件:只要是和todos相关的action,我们都触发保存行为
const storageMiddleware = store next action {
if (action.type.startsWith('todos/')) {
next(action)
todoStorage.save(store.getState().todos.value)
}
}

const store = configureStore({
// 引入我们编写的中间件
middleware: gDM gDM().concat(storageMiddleware)
})

后续更新计划

终于写完了!掌握了好多redux知识,但我们还是不满足:

  • 我们能否将TodoMVC变成多页面应用
  • 能否引入权限控制,使得只有管理员才能创建和删除待办
  • 随着Todo的逐渐复杂,看起来编辑Todo需要一个弹出表单
  • 等等...

这些功能我们会在后面的教程中带大家逐步实现,顺便,我们再学一下路由库的使用。

责任编辑:武晓燕 来源: 村长学前端
相关推荐

2022-02-11 10:16:53

5G通信数字化转型

2012-08-29 14:35:17

2020-12-22 16:10:43

人工智能

2021-08-12 07:40:05

5G5G应用5G商业模式

2010-01-12 10:14:05

龙芯

2019-10-23 19:46:31

无人驾驶谷歌自动驾驶

2022-07-19 15:27:48

元宇宙区块链货币

2022-07-04 14:28:31

5G4G数字经济

2013-05-14 16:30:00

信息化中间件

2021-09-29 16:50:04

5G通信技术

2022-07-20 09:00:00

管理项目规模化敏捷框架科技

2021-12-29 14:57:47

德勤人工智能AI驱动型企业

2017-09-03 07:00:14

2021-08-09 21:02:02

云原生规模化演进

2024-03-27 00:00:12

AI金融机构人工智能

2013-01-24 13:22:58

用友UAP云平台

2015-01-14 17:20:35

点赞
收藏

51CTO技术栈公众号