封装 mobx 6 + typescript 实现React状态管理 (3) -- mobx 封装后常见场景使用案例

系列

1. 类组件使用示例

import { disposeOnUnmount } from 'mobx-react';
import { Button } from 'antd';
import { TGlobalStore, inject } from 'portalBase/store';
import { Link } from 'react-router-dom';
import UserCreateForm from './view/UserCreateForm';
import UserList from './view/UserList';
import UserLess from './style/user.less';

interface Props {
  classicComStore: TGlobalStore['classicComStore'];
  id: string;
  type: string; // EDIT | LIST
}

interface State {
  step: number;
}

// 类组件统一使用 PureComponent 提高性能
class ClassicCom extends React.PureComponent<Props, State> {
  /**
   * 1. 不建议再使用 getDerivedStateFromProps,有其是使用他处理 可观察数据
   * 1). getDerivedStateFromProps 只有在组件决定调用 render 之前,才会调用
   * 2). 而可观察数据的改变不一定触发当前组件 render, 所以在这里获得计算值是不可控的
   * 3). 在 getDerivedStateFromProps 中使用 autorun reaction 是无效的
   * 4). 如果一定要使用计算值,可以通过 computed 在 render 实现
   * */
  static getDerivedStateFromProps(props: Props, state: State) {
    const { classicComStore } = props;
    console.log('getDerivedStateFromProps');
    return { step: classicComStore.type === 'LIST' ? 9 : 10 };
  }

  public constructor(props: Props) {
    super(props);
    this.state = { step: 0 };
  }

  public componentDidMount() {
    // 1. 如果只有did Mount 需要数据请求那么直接触发 action 即可
    // classicComStore.fetchUsers();

    // 2. 如果需要在数据变化时请求,请使用 autorun reaction when 等
    console.log('wrapper did mount');
    /**
     * 3. 建议使用 disposeOnUnmount 包装因为:
     * autorun reaction 两者一旦创建会始终追踪可观察数据变化,即使当前组件已经销毁,
     * 有可能造成内存泄漏。disposeOnUnmount 可以帮助在类组件销毁时关闭掉对可观察数据的追踪
     * */
    disposeOnUnmount(
      this,
      /**
       * 4. 建议使用 mobx.reaction,因为:
       * 1). reaction 第一个参数的返回值是否变化决定了第二个函数是否执行
       * 2). reaction 可以拿到当前数据与上一次数据,方便进行比较
       * 3). 通过设置 reaction 第三个参数 fireImmediately=true 可以立即执行一遍
       * 4). 可以模拟 didMount 与 didUpdate 场景
       * */
      mobx.reaction(
        () => {
          /**
           * 5. 建议在函数体内解构 props 与 state, 因为:
           * 1). 在函数体外层结构不能保证结构出的值是最新的,且在外层解构后不具备对 props state 简单类型值的追踪能力
           * 2). 针对类组件 props 与 state 会被转化为 observable
           * 3). 在这里解构,props 与 state 的改动就会被追踪(包括在props 定义中非 observable 的值),
           * 4). 然后通过控制返回值来决定是否执行下一个函数
           * */
          const { classicComStore } = this.props;
          const { step } = this.state;
          /**
           * 6. 建议返回简单类型数据
           * 如果返回了引用类型数据,那么需要在下一个函数中 mobx.comparer.structural 做一下比较
           * 或者 设置第三个参数的 equals: mobx.comparer.structural
           * */
          return { type: classicComStore.type, step };
        },
        (val, prevVal) => {
          const { classicComStore } = this.props;
          console.log('did mount reaction', val);
          classicComStore.fetchUsers();
        },
        /**
         * 7. 如果需要同时模拟 didMount didUpdate 则 追加 fireImmediately: true
         * 如果第一个参数会返回一个引用类型值,那么可以考虑配置 equals,或者在第二个函数中自定义判断
         * */
        { fireImmediately: true, equals: mobx.comparer.structural },
      ),
    );
  }
  /**
  * 不再建议借助 componentDidUpdate 判断前后数据改变来做其他处理
  * 1). 可观察数据改变不一定触发 render 及 update
  * 2). 如果借助 autorun reaction 则每次处罚 componentDidUpdate 都会重新创建一个追踪
  */
  public componentDidUpdate() {}

  public render(): React.ReactNode {
    const { classicComStore } = this.props;
    console.log('render Container');
    return (
      <div className={UserLess.container}>
        <div className={UserLess.content}>
          <Button onClick={() => classicComStore.resetType()}>change store type</Button>
          <Button onClick={() => this.setState({ step: 1 })}>change state step</Button>
          <Link to="/classic/test?type=EDIT">/classic/test?type=EDIT</Link>
          <UserCreateForm classicComStore={classicComStore} />
          <UserList classicComStore={classicComStore} />
        </div>
      </div>
    );
  }
}

// 插入 store
export default inject((store) => ({
  classicComStore: store.classicComStore,
}))(ClassicCom);

2. 函数式组件使用示例

/**
 * 函数式组件 mobx 使用场景示例
 * */
import { Observer } from 'mobx-react';
import { Button } from 'antd';
import { TGlobalStore, inject } from 'portalBase/store';
import { Link } from 'react-router-dom';
import UserCreateForm from './view/UserCreateForm';
import UserList from './view/UserList';
import UserLess from './style/user.less';

interface Props {
  funcComStore: TGlobalStore['funcComStore'];
  id: string;
  type: string; // EDIT | LIST
}

const FuncCom: React.FC<Props> = (props: Props) => {
  const { funcComStore, type } = props;

  // 简单类型的 state 没必要使用 useLocalObservable
  const [step, setStep] = React.useState(0);

  // 可以使用 useLocalObservable 代替 useState 存储 state
  // const stepObs = useLocalObservable(() => {
  //   step: 0,
  //   increaseStep() {
  //     this.step = 1
  //   }
  // })

  // 1. 如果只有 首次渲染需要请求数据那么直接在 useEffect 中触发 action 即可
  // React.useEffect(() => funcComStore.fetchUsers(), []);

  /**
   * 2. 如果需要在数据变化时重新请求数据,则要结合 autorun reaction when 等使用
   * 1). autorun reaction when 等的运行返回值,是追踪器的 dispose() 销毁函数
   * 2). useEffect 第一个参数的返回值函数会在组件卸载时调用
   * 3). 把 autorun reaction when 的运行返回值作为 useEffect 第一个参数的返回值,刚好可以实现在组件卸载时 dispose 关掉追踪器
   * */
  React.useEffect(
    () =>
      /**
       * 3. 推荐使用 mobx.reaction, 因为:
       * 1). 触发重新请求数据的 "可观察数据",不一定是需要传递给请求函数的(也就是说可能不被使用到)
       * 2). autorun 只能在当前函数体使用到 "可观察数据" 改变时触发,因此不适合使用
       * 3). 设置 reaction 第三个参数 fireImmediately=true 可以达到立即执行一遍的效果
       * */
      mobx.reaction(
        () => {
          /**
           * 4. 不建议在第一个函数体内解构 props, state, 因为:
           * 1). 与类组件不同,函数式组件的 props, state 不会被自动转化为 observable 因此在这个函数体内解构,不会出发更新
           * 2). 如果有非 "可观察数据" 作为判断条件,建议直接传给 React.useEffect 第二个参数 deps,
           * 3). 这样函数执行的条件就变成了: "可观察数据" 改变 + useEffect deps依赖的 "不可观察数据" 改变
           * 4). "不可观察数据" 的改变,会触发 useEffect 执行,同时会传入新的 props state, 因此不用在这里解构
           * */
          // const { step } = this.state;
          console.log('in expression');
          /**
           * 5. 同样建议返回简单类型数据,方便计算,如果返回引用类型 则需要在下一个函数中 做判断
           * 或者 设置第三个参数的 equals: mobx.comparer.structural
           * */
          return { storeType: funcComStore.type, type, step };
        },
        (val) => {
          console.log('did mount reaction', val);
          funcComStore.fetchUsers();
        },
        /**
         * 6. 如果需要同时模拟 didMount didUpdate 则 追加 fireImmediately: true
         * 如果第一个参数会返回一个引用类型值,那么可以考虑配置 equals,或者在第二个函数中自定义判断
         * */
        { fireImmediately: true, equals: mobx.comparer.structural },
      ),
    /**
     * 7. userEffect 的第二个参数表示执行 effect 的依赖条件
     * 1). 如果依赖条件只有 "可观察数据" ,那么这里不需要填保持 [] 即可
     * 2). 如果依赖条件包含 "非可观察数据",那么这里需要加入 "非可观察数据"
     * */
    [type, step],
  );
  console.log('render Container');

  /**
   * React.useCallback 用来确保 方法的指针是不变的,减少无意义的 render
   * */
  const renderUserName = React.useCallback(() => {
    return (
      /**
       * 如果我们组件中有在组件的回调函数中用到 ”可观察数据“ 那么需要 <Observer> 组件来协助
       * 否则不会达到预期效果
       * */
      <Observer>{() => <div>{funcComStore.users.map((u) => u.name).join(', ')}</div>}</Observer>
    );
  }, []);

  return (
    <div className={UserLess.container}>
      <div className={UserLess.content}>
        <Button onClick={() => funcComStore.resetType()}>change store type</Button>
        <Button onClick={() => setStep(1)}>change state step</Button>
        <Link to="/func/test?type=EDIT">/func/test?type=EDIT</Link>
        <p>userNum: {funcComStore.userNum}</p>
        <UserCreateForm funcComStore={funcComStore} renderUserName={renderUserName} />
        <UserList funcComStore={funcComStore} />
      </div>
    </div>
  );
};

export default inject((store) => ({
  funcComStore: store.funcComStore,
}))(FuncCom);

3. 类组件与 函数式组件的 Ref 相关使用案例

/**
    1. 类组件, 类组件自带 ref, 即使被我们 inject()(ClassicCom) 依然可以正常使用 ref
    2. 函数式组件,函数式组件需要经过 React.forwardRef 包装
*/ 

// 1. 经过 inject 包装的类组件
const InjectClassicCom = inject((store) => ({
  classicComStore: store.classicComStore,
}))(ClassicCom);

// 2. 经过 convert 包装的类组件
const ConvertClassicCom = convert(ClassicCom);

// 函数式组件使用 ForwardRef 的示例
interface RefParams {
  changeSomething: () => void;
}

// 注意函数组件类型为 ForwardRefRenderFunction
const FuncWithRefCom: React.ForwardRefRenderFunction<RefParams, Props> = (
  props: Props,
  ref: React.ForwardedRef<RefParams>,
) => {
 
  useImperativeHandle(ref, () => ({
    changeSomething: () => console.log('change something in functional')
  }));

  return <div/>;
};

// 3. 经过 inject 包装的带有 forwardRef 的 函数式组件
const InjectFuncWithRefCom = inject((store) => ({
  funcRefComStore: store.funcRefComStore,
}))(React.forwardRef(FuncWithRefCom));

// 4. 经过 convert 包装的带有 forwardRef 的函数式组件
const ConvertFuncWithRefCom = convert(React.forwardRef(FuncWithRefCom))


// 5. ref 的使用示例
const RouteEntry: React.FC = () => {
  const injectClassicRef = React.useRef<React.ElementRef<typeof InjectClassicCom>>(null);
  const convertClassicRef = React.useRef<React.ElementRef<typeof ConvertClassicCom>>(null);
  const injectFuncRef = React.useRef<React.ElementRef<typeof InjectFuncWithRefCom>>(null);
  const convertRuncRef = React.useRef<React.ElementRef<typeof ConvertFuncWithRefCom>>(null);

  function handleClick() {
    console.log(injectClassicRef.current?.changeSomething());
    console.log(convertClassicRef.current?.changeSomething());
    console.log(injectFuncRef.current?.changeSomething());
    console.log(convertRuncRef.current?.changeSomething());
  }

  return (
    <div>
        <Button onClick={handleClick}>ref click</Button>
        <InjectClassicCom ref={injectClassicRef} />
        <ConvertClassicCom ref={convertClassicRef} />
        <InjectFuncWithRefCom ref={injectFuncRef} />
        <ConvertFuncWithRefCom ref={convertRuncRef} />
    </div>
  );
};

export default RouteEntry;

4. 其他 Mobx 使用注意事项

Mobx 中维护的 ”可观察数据“ 已经与我们常用的 ES6 数据不太一样了,虽然它们用大部分一样的 Api 例如 数组的 map forEach 等,但是在某些情况会出现与我们预期不一致的地方,下面会列举开发中会遇到的问题,以及解决方法

  1. convert 包裹的组件的子组件在 mobx state 改变后不触发 render ?
    Mobx-react 提供的 observer 仅能作用到被包裹的组件,其子组件要想实现响应式,也需要再用 observer 包裹一下;convert 是对 observer 的封装,具有同样的行为
  2. 尽可能晚的从 mobx 对象中获取值
    与 redux 不同,mobx 的 observer 会对我们读取 state 的值来进行跟踪,如果在组件外读取值,然后传入到 observer 组件内,则 observer 组件内没法获得跟踪,越晚的取值,触发更新的组件会越少
  3. 如何给第三方组件(或者不使用 observer的组件)传递 mobx state ?
    直接传入 mobx state 中的复杂类型值可能只会在首次起作用,这是应为 mobx state 中引用类型数据的指针是不变的,并且与普通 JS 数据类型不同,直接给第三方使用可能会引起错误,因此正确的方法是将数据经过 mobx.toJS() 转换一下
  4. 组件的参数是个回调函数,回调函数中使用了 mobx state ,结果没有触发更新?
    如果组件的参数是一个回调函数,同时回调函数中用到了“可观察对象”,那么需要确保回调函数中的组件 是一个 observer, 或者使用 ‘mobx-react’ 提供的 <Observer> 包裹一下
  5. Mobx state 数据类型怎么判断, 为什么不能用 Array.isArray 判断数组?
    Mobx state 中的数据只是看着与 JS 数据一样,而且大部分的使用都一样,但是在判断 数据类型时最好使用 mobx 提供的方法: 数据计算与判断相关 Api 例如:判断数组应该用 mobx.isObservableArray() 或者将数组 .slice() | toJS() 后再次判断, 使用 Array.isArray 在支持 Proxy 的浏览器中能够得到预期的结果,但是在不支持Proxy 的浏览器中 mobx 会降级使用其他方式实现,其结果就是 Array.isArray 的结果不符合预期
  6. 类组件的 state 需不需要手动转换成 observable ?
    实际是不需要且不建议的,因为如果 类组件通过 observer 或者 inject 变成 ”观察者“后,其属性 props 与 state 会被自动变成 observable ,类组件跟踪这两者的变化,来决定是否 render
  7. 类组件只有 render 中用到的 可观察数据会被追踪,函数式组件同样只有最外层用到的 可观察数据会被追踪
    如果想在类组件的其他声明周期或者函数式组件的 useEffect 中追踪 ”可观察数据“ 需要通过 autorun reaction when 等 api 包装
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值