给 RN 添加 mobx

背景

项目组中 RN 之前使用的状态管理是小伙伴自行封装 redux, redux-saga 的一个组件库,然而由于文档不全,使用起来不方便等原因,经过讨论,决定替换成 mobx。主要参考官方文档: https://mobx.js.org/react-integration.html#react-integration。本文记录我将 mobx 加入 RN 的过程以及使用 mobx 的一些注意事项(实际上与普通的 React 项目差不多)

过程

  1. 在 src 目录下新增一个 stores 文件夹用于存放 mobx 相关的代码

  2. 编写 observable state,这里我写了一个 Counter.ts 并且新建 stores.ts 管理所有的状态:

    // Counter.ts
    import { makeAutoObservable } from 'mobx';
    
    class Counter {
      count = 0;
    
      constructor() {
        makeAutoObservable(this);
      }
    
      // 注意要使用箭头函数,否则需要用 action.bound 来绑定 this,否则指向 React 组件
      increase = () => {
        this.count += 1;
      };
    
      decrease = () => {
        this.count -= 1;
      };
    }
    export default Counter;
    
    // stores.ts
    import Counter from './Counter';
    
    export default {
      counter: new Counter(),
    };
  3. 根据 mobx 文档,组件要使用外部状态有三种写法:

    1. 通过 props 将 状态传递到组件
    2. 直接通过 import 进来
    3. 通过 React context 引入到组件

    为了方便使用,考虑 2,3 中写法,文档中推荐使用 context,那么就这里尝试使用 context。

  4. 编写 withStores.ts 函数,这是一个高阶组件,主要的作用是给根元素用 Provider 包裹起来,并且根元素通常也是需要 observe 的,因此这里也顺便将其转成可观测的组件:

    // withStores.ts
    import React, { createContext } from 'react';
    import { observer } from 'mobx-react-lite';
    import stores from './stores';
    
    // 初始化 Context
    export const StoresContext = createContext<typeof stores>(stores);
    
    const withStores = <P extends object>(
      WrappedComponent: React.FunctionComponent<P>,
    ) => (props: P) => {
      // 转成可观测的组件
      const ObserverWrappedComponent = observer(WrappedComponent);
      return (
        <StoresContext.Provider value={stores}>
          <ObserverWrappedComponent {...props} />
        </StoresContext.Provider>
      );
    };
    
    export default withStores;
  5. 接下来的使用就简单了:

    import React, { useEffect, useContext } from 'react';
    import { TouchableOpacity, View, Text } from 'react-native';
    import { withStores, StoresContext } from '@/stores';
    import Child from './Child';
    
    const MobxDemo = () => {
      const { counter } = useContext(StoresContext);
      return (
        <View>
          <Text>count: {counter.count}</Text>
          <TouchableOpacity onPress={counter.increase}>
            <Text>increase: +</Text>
          </TouchableOpacity>
          <TouchableOpacity onPress={counter.decrease}>
            <Text>decrease: -</Text>
          </TouchableOpacity>
          <Child />
        </View>
      );
    };
    
    export default withStores(MobxDemo);
    
    // Child.tsx
    import React, { useContext } from 'react';
    import { observer } from 'mobx-react-lite';
    import { Text } from 'react-native';
    import { StoresContext } from '@/stores';
    
    export const Child = () => {
      const { counter } = useContext(StoresContext);
      return <Text>child: {counter.count}</Text>;
    };
    
    export default observer(Child);