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