使用 React&Mobx 的几个最佳实践

Mobx 是我非常喜欢的 React 状态管理库,它非常灵活,同时它的灵活也会给开发带来非常多的问题,因此我们在开发的时候也要遵循一些写法上的最佳实践,使我们的程序达到最好的效果。
在 store 中维护业务逻辑
尽量不要把业务逻辑写在 React Component 里面。当你把业务逻辑写在组件里面的时候,很难及时定位错误的,因为业务逻辑分散在各种不同的组件里面,让你很难来通过行为来定义到底是哪些代码涉及的这个错误,不同组件复用这些逻辑也很困难。
最好在 stores 中把业务逻辑编写成方法,并在你的 Component 中调用这些方法。
只允许在 store 中修改属性
尽量不要在一个 Component 里直接修改一个 store 的属性。只有 store 本身可以修改他自己的属性。
当你要改变属性的时候,请调用相应的 store 方法。不然的话你的属性修改会散落在各处不受控制,这是很难调试的。
class Store {
  @observable text;
  
  @action.bound
  handleSearch = value => {
    this.text = value
  }
}
const store = new Store();
@observer
class Home extends Component {
  
  handleChanged = (event) => {
    store.handleSearch(event.target.value);
  }
  
  render() {
    return (
      <input
        value={store.searchText}
        onChange={this.handleChanged}
      />
    );
  }
}
所有属性更改都用 action
使用 action 后,可以清楚的看出哪些代码可以更改可观察的变量,并且方便调试工具给出更多的信息
使用 transaction 可以将多个应用状态(Observable)的更新视为一次操作,并只触发一次监听者(Reactions)的动作(UI更新、网络请求等),避免多次重复渲染。action中封装了transaction,多次改变@observable变量时,只会重新渲染一次,提高了性能。
class Store {
  @observable name;
  @observable age;
  @action
  change(name,age){
    this.name = name;
    this.age = age;
  }
}
从 store 中分离出 API 请求
不要在你的 store 里调用 API 接口,这会让它们很难测试,也让代码变的更复杂和耦合。额外建一个类,把 API 接口调用放进去,并在 store 的构造函数里实例化他们来使用。当你编写测试代码时,你可以很容易地模拟这些 api 并把你的模拟 api 实例传给每一个 store。
class UserService {
  fetchUser = () => axios.get('/api/user')
}
class Store {
  @observable todos = [];
  constructor(userService) {
    this.userService = userService;
  }
  fetchUser = async () => {
    const todos = await this.userService.fetchUser();
    runInAction(() => {
      this.todos = todos;
    });
  }
}
const userService = new UserService();
const store = new Store(userService);
对每一个 component 都声明 @observer
@observer 可以用来将 React 组件转变成响应式组件。它用 mobx.autorun 包装了组件的 render 函数以确保任何组件渲染中使用的数据变化时都可以强制刷新组件。
给每一个 component 都标注 @observer ,这可以使得他们可以随着 store prop 的改变而更新。如果子组件没有标注 @observer 的话,就会导致其父 component (有 @observer )刷新。因此我们要尽可能的让每个子 component 都标注 @observer ,这可以减少不必要的重新渲染。
不要缓存 observables 属性
Observer 组件只会追踪在 render 方法中存取的数据。如果你从 observable 属性中提取数据并将其缓存在组件里,这样的数据是不会被追踪的:
class Store {
  @observable name;
  @observable age;
}
class Home extends React.Component {
  componentWillMount() {
    // 错误的,info 的更新不会被追踪
    this.info = store.name + store.age
  }
  render() {
    return <div>{this.info}div>
  }
}
使用 @computed
比如刚刚的例子,使用 @computed 属性来处理一些涉及多个属性的逻辑。使用 @computed 可以减少这样的判断类业务逻辑在组件里面出现的频率。
class Store {
  @observable name;
  @observable age;
  @computed info = () => {
    return this.name + this.age;
  }
}
class Home extends React.Component {
  render() {
    return <div>{store.info}div>
  }
}
多编写受控组件
多编写可控组件,这样会大大降低你的测试复杂度,也让你的组件易于管理。
当需要追踪对象属性时、使用 map
MobX 可以做许多事,但是它无法将原始类型值转变成 observable (尽管可以用对象来包装它们)。所以说值不是 observable,而对象的属性才是。这意味着 @observer 实际上是对间接引用值作出反应。所以如果像下面这样初始化的话,Timer 组件是不会作出任何反应的:
ReactDom.render(<Timer timerData={timerData.secondsPassed} />, document.body)
在这行代码中,只是 secondsPassed 的当前值传递给了 Timer,这个值是不可变值 (JS中的所有原始类型值都是不可变的)。这个值永远都不会改变,所以 Timer 也永远不会更新。 secondsPassed 属性将来会改变,所以我们需要在组件内访问它。或者换句话说: 永远只传递拥有 observable 属性的对象。
如果你想追踪对象中每个属性的变更,可以使用 map:
observable.map(values?) 创建一个动态键的 observable 映射。如果你不但想对一个特定项的更改做出反应,而且对添加或删除该项也做出反应的话,那么 observable 映射会非常有用。
class Store {
  timerData = @observable.map({secondsPassed:0});
  @action.bound
  change(value){
    this.timerData.set('secondsPassed',value);
  }
}
class Home extends React.Component {
  render() {
    return <div>{store.timerData.get('secondsPassed')}div>
  }
}推荐阅读
