之前和大家分享了我们不需要系列:

接下来我们继续调整难度, 替换应用范围更广的库: redux, react-redux

《我们或许不需要...》系列如果是做轮子就没有意义了, 此系列目的是通过简单的代码, 对原有库的设计思路进行概括提取, 最终从理解其理念更高效的开发项目的过程.

此行目的

Redux 在 React 中的重要性在此不再暂开, 其设计理念(单向数据流, 提供者模式)深得人心, 但是在实际开发中, 每个引用 Redux 的项目都会需要解决以下三个问题:

  • 如何设计状态管理在工程中的模块结构
  • 如何在不影响设计结构的前提下减少编写 action, reducer 的模板代码
  • 如何减少不必要的重绘(immutable)

我们最后会基于 Context, 使用 20 行代码和一点点规范满足以上目标

本文中提到的代码都可以直接粘贴至项目中进行验证.

redux 官方最佳实践

首先编写项目入口

// src/index.js
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import Home from './Home';
import ChangerBar from './ChangerBar';
import store from './store';

// 我们在项目的最外层包裹一个 Provider 对象, 以实现提供者模式
function App() {
  return (
    <Provider store={store}>
      <div>
        <ChangerBar />
        <Home />
      </div>
    </Provider>
  );
}

render(<App />, document.getElementById('root'));

接下来初始化状态, 并且编写 reducer, 根据后续 dispatch 传递的 action 对象修改状态

// src/store.js
import { createStore } from 'redux';

const defaultStore = {
  name: 'dog',
  friends: ['cat', 'fish'],
  age: 100,
};

function reducer(store = defaultStore, action) {
  switch (action.type) {
    case 'changeName':
      store = { ...store, name: action.name };
      break;
    case 'addAge':
      store = { ...store, age: store.age + 1 };
      break;
    default:
      break;
  }
  return store;
}

const store = createStore(reducer);

store.dispatch({ type: 'init' });

export default store;

接下来我们创建 actions 文件, 里面是 action 的集合

// src/actions.js
export function changeName(name) {
  return {
    type: 'changeName',
    name,
  };
}

export function addAge() {
  return {
    type: 'addAge',
  };
}

我们绘制一个组件, 用来修改全局状态

react-redux 提供了一个 connect 组件, 它其实是一个 HOC(高阶组件), redux 这样的设计目的有两个:

  1. 监听和释放对 store 的订阅的行为被封装在 HOC 中, 这样可以不必每次都编写此逻辑代码;
  2. 将 state 和 disptch 对象转换为 props 注入至组件中, 而不是由组件去引用外部的对象, 这样组件内部只有一个概念就是 props.
// src/ChangerBar.js
import React from 'react';
import { connect } from 'react-redux';
import * as actions from './actions';

const ChangerBar = ({ changeName, addAge }) => {
  function handleOnChange(e) {
    changeName(e.target.value);
  }

  return (
    <div>
      <div>bar</div>
      <input placeholder="修改姓名" onChange={handleOnChange} />
      <button type="button" onClick={addAge}>
        更新年龄
      </button>
    </div>
  );
};

// 将 dispatch 注入到组件Props中
function mapDispatchToProps(dispatch) {
  return {
    addAge: () => dispatch(actions.addAge()),
    changeName: name => dispatch(actions.changeName(name)),
  };
}

export default connect(
  null,
  mapDispatchToProps,
)(ChangerBar);

实现 Home 组件, 订阅数据, 验证状态管理(UI 更新)

// src/Home.js
import React from 'react';
import { connect } from 'react-redux';

const Home = ({ name, age }) => {
  return (
    <div>
      <div>name: {name}</div>
      <div>age: {age}</div>
    </div>
  );
};

// 将 state 的值注入到 props 中
function mapStateToProps(state) {
  // 当任何一个 dispatch() 执行时, 此处将会重新运行, 并且注入新的 name 和 age 至组件 props 中, 以更新组件
  return {
    name: state.name,
    age: state.age,
  };
}

export default connect(mapStateToProps)(Home);

以上代码相信有一定经验的 React 开发者已经非常熟悉, 当项目逐渐复杂时, 我们会逐步修改项目结构, 常见的有两种:

  1. 鸭子模式, 将状态管理分布在一个个页面中, 跨页面的状态管理提升至全局, dva 使用的是此模式, 每个需要使用全局状态的页面都会有一个自己的 action 和 reducer
  2. 中心化的状态管理: 将整个 actions 和 reducer 规整到一个全局目录中, actions 以事件为约定, 而不是以页面

这两种方式各有千秋, 鸭子模式的缺点是我们无法避免有跨页面的状态管理情况发生, 所以状态会被分布在全局和局部两处.

以上模式还有一个弊端就是 action.type 我们需要保持一致, 当 action 过多时, 可能需要一个 types 的文件用来存储 action.type 常量, 如此一来我们每编写一个新的状态需要:

  1. 打开 types 文件, 添加一个 action.type 常量
  2. 打开某个 action 文件, 引入 types, 编写 action
  3. 打开某个 reducer 文件, 引入 types, 编写 reducer
  4. 打开容器组件文件, 引入 connect, actions, 编写状态的获取和触发更新

以上还是没有引入 sage 和 immutable , 写到这里已经感受到我们 react 开发者正处于水深火热之中, 我们得加紧步伐.

接下来我们抛弃 redux 重写以上代码

利用 context 实现 react-redux 类似功能

redux 作者在 context API 更新之后, 提到过, 有了 context 我们可以不需要 redux 了, 所言非虚.

我们创建一个类似 createStore 的函数, 此函数会创建一个 Provider 和一个 store, 我们要利用不可变数据减少不必要的的重绘, 这里使用 immer:

// src/createContextRedux.js
import React, { createContext, useMemo } from 'react';
import immer from 'immer';

export default function createContextRedux() {
  // 创建一个  context, 用于后续配合 useContext 进行更新组件
  const store = createContext();

  // 创建一个提供者组件
  const Provider = ({ defaultState = {}, ...rest }) => {
    const [state, setState] = React.useState(defaultState);

    // 仅有 state 变更了, 才会重新更新 context 和 store
    return useMemo(() => {
      // 使用 immer 进行更新状态, 确保未更新的对象还是旧的引用
      const dispatch = fn => setState(immer(state, v => fn(v)));

      store.state = state;
      store.dispatch = dispatch;

      return <store.Provider value={state} {...rest} />;
    }, [state]);
  };

  return { Provider, store };
}

好的, 这 20 行代码就是状态管理库的全部, 接下来我们利用它去实现刚刚的业务

重写 store, 我们引入刚刚编写的状态管理库, 然后创建全局 Provider 和 store:

// src/store.js
import createContextRedux from './createContextRedux';

const { Provider, store } = createContextRedux();

export { Provider, store };

在项目最顶层使用 Provider 包裹, 以提供 context

// index.js
import React from 'react';
import { render } from 'react-dom';
import { Provider } from './store';
import Home from './Home';
import ChangerBar from './ChangerBar';

function App() {
  return (
    <Provider>
      <ChangerBar />
      <Home />
    </Provider>
  );
}

render(<App />, document.getElementById('root'));

这里我们移除了 reducer, 只有 action 的概念, 可以简化非常多的代码量

我修改 actions.js 文件, 由于当前只有 action 的概念, 所以非常适合使用 action 中心化的方式, 将容器组件的 action 都汇集放置一处, 容器组件仅读取状态和调用 action

// src/actions.js
import { store } from './store';

export function changeName(name) {
  // 我们直接修改状态对象即可, 该函数会利用 immer 创建一个新的对象返回, 没有被修改的子对象还是旧的引用
  store.dispatch(state => {
    state.name = name;
  });
}

export function addAge() {
  store.dispatch(state => {
    if (state.age === void 0) {
      state.age = 0;
    }
    state.age += 1;
  });
}

ChangerBar 不需要关联状态, 它只需要引用 actions 即可

// src/ChangerBar.js
import React from 'react';
import * as actions from './actions';

const ChangerBar = () => {
  // 使用 hook 获取 context, 代替 conncet

  function handleOnChange(e) {
    actions.changeName(e.target.value);
  }

  function handleAddAage() {
    actions.addAge();
  }

  return (
    <div>
      <div>bar</div>
      <input placeholder="修改姓名" onChange={handleOnChange} />
      <button type="button" onClick={handleAddAage}>
        更新年龄
      </button>
    </div>
  );
};

export default ChangerBar;

最后在 Home 页面读取全局数据, 监听全局修改, 使用 useContext 代替 connect

// src/Home.js
import React from 'react';
import { store } from './store';

const Home = () => {
  const { name, age } = React.useContext(store);

  return (
    <div>
      <div>name: {name}</div>
      <div>age: {age}</div>
    </div>
  );
};

export default Home;

有时候序也可以写在结尾, 不是么?

上面可能贴的代码太多了, 并且较为分散, 我们把它聚合成两个文件重新阅读:

实现状态管理

import React, { createContext, useMemo } from 'react';
import immer from 'immer';

export default function createContextRedux() {
  // 创建一个  context, 用于后续配合 useContext 进行更新组件
  const store = createContext();

  // 创建一个提供者组件
  const Provider = ({ defaultState = {}, ...rest }) => {
    const [state, setState] = React.useState(defaultState);

    // 仅有 state 变更了, 才会重新更新 context 和 store
    return useMemo(() => {
      // 使用 immer 进行更新状态, 确保未更新的对象还是旧的引用
      const dispatch = fn => setState(immer(state, v => fn(v)));

      store.state = state;
      store.dispatch = dispatch;

      return <store.Provider value={state} {...rest} />;
    }, [state]);
  };

  return { Provider, store };
}

使用状态管理

// src/index.js
import React from 'react';
import { render } from 'react-dom';
import createContextRedux from './createStore';

const { Provider, store } = createContextRedux();

// 模拟一个异步
function fetchData() {
  return new Promise(res => {
    setTimeout(() => {
      res();
    }, 500);
  });
}

// 一个基础的 action, 用来修改状态
// 在实际项目中, action 应该统一放置一处, 不应该分散在各组件中
async function actionOfAddNum() {
  await fetchData();

  store.dispatch(state => {
    state.age += 1;
  });
}

// 点击之后, 利用 action 修改全局状态
function Changer() {
  return (
    <button type="button" onClick={actionOfAddNum}>
      add
    </button>
  );
}

// 利用 useContext 监听全局状态, 并随时进行更新
function Shower() {
  const { age } = React.useContext(store);

  return <div>age: {age}</div>;
}

function App() {
  return (
    <Provider defaultState={{ age: 0 }}>
      <Shower />
      <Changer />
    </Provider>
  );
}

render(<App />, document.getElementById('root'));

最终我们移除了 redux, 在确保 单向数据流 的状态逻辑上, 移除了 reducer, connect 的步骤, 将模板代码减少至一份: 编写 action;

通过分离 action 和组件, 解耦状态变更和更新.

谢谢观看