杨sir

V1

2022/11/23阅读:32主题:绿意

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

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

image-20221107103643254
image-20221107103643254

前言

小羊们好!在上一篇文章中,我们通过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,很不好写吧?
image-20221108172519067
image-20221108172519067

我们期待的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: {
    value0
  },
  // `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 onClick={() => dispatch(decrement())}>-</button>
      <span>{count}</span>
      <button onClick={() => dispatch(increment())}>+</button>
    </div>

  )
}

看看效果吧!

2022-11-08 20-13-56.2022-11-08 20_14_12
2022-11-08 20-13-56.2022-11-08 20_14_12

重构Todo应用

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

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

const initialState = {
  valueJSON.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,
        completedfalse,
      });
    },
    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)}
          onClick={() =>
 setFilter(VisibilityFilters.SHOW_ALL)}
        >
          All
        </button>
      </li>
      <li>
        <button
          className={getSelectedClass(VisibilityFilters.SHOW_ACTIVE)}
          onClick={() =>
 setFilter(VisibilityFilters.SHOW_ACTIVE)}
        >
          Active
        </button>
      </li>
      <li>
        <button
          className={getSelectedClass(VisibilityFilters.SHOW_COMPLETED)}
          onClick={() =>
 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中只需要替换selectTodosselectFilteredTodos即可:

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({
  // 引入我们编写的中间件
  middlewaregDM => gDM().concat(storageMiddleware)
})

后续更新计划

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

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

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

写在最后

欢迎 长按图片加好友,我会第一时间和你分享 前端行业趋势面试资源学习路径 等等。

添加好友备注【进阶学习】拉你进羊村前端学习群,和大佬们一起学习,关注课程更新!

关注公众号后,在首页:

  • 输入“面试题”获取2022最新面试题
  • 输入“进群”跟500位前端大佬一起卷
  • 输入“简历点评”参加直播点评活动
  • 输入“宝藏卡”开启免费学习之旅
  • 输入“视频教程”查看村长精品私教课

分类:

前端

标签:

React.js

作者介绍

杨sir
V1