「✍ Mobx」搭配 React + Typescript食用

·  阅读 2468

前言

之前项目中状态管理清一色的都是用redux,各种redux实践也尝试过,但是给人感觉还是特别的重(redux-toolkit还不错),因此入坑尝试口碑不错的mobx

Begin

创建一个Store

这里我们以一个todolist为例子

import { observable, action, makeObservable } from 'mobx';

class Todo {
  constructor() {
    // mobx6.0后的版本都需要手动调用makeObservable(this),不然会发现数据变了视图不更新
    makeObservable(this); 
  }
  @observable list = [
    {
      label: 'exec',
      finish: false,
    },
    {
      label: 'study',
      finish: false,
    },
  ];

  @action
  finsh(label: string) {
    let list = [...this.list];
    list.map(item => {
      if (item.label === label) {
        item.finish = true;
      }
      return item;
    });
    this.list = list;
  }
}
const todoStore = new Todo();
export default todoStore;
复制代码

@observable

创建一个可响应的变量,注意这里并不是原始变量

@action

这其实就是redux中的action,据说老版本中mobx是直接修改state的,听起来就非常不安全。这种使用action触发动作的形式似乎更能让人接受

编写业务组件

获取响应式数据,我们一般有两种形式,一种是直接引入定义的Store, 一种是利用Provider和inject来注入到组件的props中

直接引入store

import React from 'react';
import { observer } from 'mobx-react';
import todoStore from '../../store/todo'

@observer
class Todo extends React.Component{
  render() {
    return (
      <div className="todolist">
        <div className="unfinish">
          {todoStore.list.map(item => (
            <div key={item.label} style={{ display: 'flex' }}>
              {!item.finish && (
                <>
                  <span>{item.label}</span>
                  <button onClick={() => todoStore.finsh(item.label)}>do</button>
                </>
              )}
            </div>
          ))}
        </div>

        <div className="finish">
          {todoStore.list.map(item => (
            <div key={item.label}>{item.finish && <span>{item.label}</span>}</div>
          ))}
        </div>
      </div>
    );
  }
}

export default Todo;
复制代码

父组件

<div>
  <Todo />
</div>
复制代码

使用Provider和inject

import React from 'react';
import { observer, inject } from 'mobx-react';

@inject('todoStore')
@observer
class Todo extends React.Component{
  render() {
    const { todoStore } = this.props
    return (
      <div className="todolist">
        <div className="unfinish">
          {todoStore.list.map(item => (
            <div key={item.label} style={{ display: 'flex' }}>
              {!item.finish && (
                <>
                  <span>{item.label}</span>
                  <button onClick={() => todoStore.finsh(item.label)}>do</button>
                </>
              )}
            </div>
          ))}
        </div>

        <div className="finish">
          {todoStore.list.map(item => (
            <div key={item.label}>{item.finish && <span>{item.label}</span>}</div>
          ))}
        </div>
      </div>
    );
  }
}

export default Todo;

复制代码

父组件

import todoStore from '../store/todo';
<Provider todoStore={todoStore} >
    <Todo />
</Provider>
复制代码

inject的方式有个缺点,typescript支持不太好,注入之后还得手动写props的类型,体验一般

Use In Function Component

关于这一块 zhuanlan.zhihu.com/p/138226768 讲得很好

全局Store & observer

store/person

const person = observable({ name: 'John' })
export default person
复制代码
import { observable } from 'mobx'
import { observer } from 'mobx-react'

import person from './store/person'

const Person = observer(function Person() {
  return (
    <div>
      {person.name}
      <button onClick={() => (person.name = 'Mike')}>No! I am Mike</button>
    </div>
  )
})
复制代码

useObserver & useLocalStore with Hook

我们也可以使用hook的写法, 使用useLocalStore来创建一个组件独立的store,useObserver包裹render使其能够响应式渲染。注:useObserver不仅仅只可以像demo一样包裹render,你可以用在你的自定义函数中,像useMemo一样

import { useObserver, useLocalStore } from 'mobx-react'
function Person() {
  const person = useLocalStore(() => ({ name: 'John' }))
  return useObserver(() => (
    <div>
      {person.name}
      <button onClick={() => (person.name = 'Mike')}>No! I am Mike</button>
    </div>
  ))
}
复制代码

为什么使用useLocalStore呢

It creates a local observable that will be stable between renderings. You can use if with functional components. In the example, you can see that the component doesn't depend on any react context or external store, but it's still using mobx and it's completely self-contained.

stackoverflow.com/questions/6…

简而言之就是你的store不依赖外部的store,相对独立,可以把useLocalStore看作mobx中的useReducer

全局Store & useObserver

useObserver也可以搭配App Store,也就是全局的Store使用

person store


import { observable } from 'mobx'

const person = observable({
  name: 'jenson',
  age: 20,
  changeName: (name: string) => {
    person.name = name
  }
})

export default person
复制代码

use

import React from 'react'
import {useObserver} from 'mobx-react'
import personStore from '../../store/person'
const Person: React.FC = () =>{

  return useObserver(()=>(
    <div>
      <span>{personStore.name}</span>
      <span>{personStore.age}</span>
      <button onClick={()=>personStore.changeName('xixi')}>changeName</button>
    </div>
  ))
}
export default Person
复制代码

useLocalObservable

最近mobx做出更新,useLocalStore即将废弃, 使用useLocalObservable代替

demo

import React, { useCallback, useContext } from 'react';
import { useLocalObservable, useObserver } from 'mobx-react';

const Person: React.FC = () => {
  const store = useLocalObservable(() => ({
    count: 1,
    addCount: () => (store.count += 1),
  }));

  return useObserver(() => (
    <div>
      {store.count}
      <button onClick={store.addCount}>add count</button>
    </div>
  ));
};
export default Person;

复制代码

render机制

当你没有使用useObserver 或是在 useObserver中没有使用到store中的数据,即使store改变了,也不会触发组件的render

这样写可以触发render

import React, { useCallback, useEffect } from 'react';
import { useLocalObservable, useObserver } from 'mobx-react';
import personStore from '../../store/person';
import { runInAction } from 'mobx';

const fetchData = () => new Promise(resolve => setTimeout(() => resolve(0), 500));

const Person: React.FC = () => {
  console.log('render');
  const handleFetchData = useCallback(async () => {
    await fetchData();
    runInAction(() => {
      // 这里借助 mobx 的 action,可以很好的做到批量更新,此时组件只会更新一次
      personStore.changeName('change name from data');
      personStore.changeAge(12);
    });
  }, []);

  // 触发render
  const name = useObserver(() => 'the name is ' + personStore.name);
  const age = useObserver(() => 'the age is ' + personStore.age);

  return (
    <div>
      <h3>{name}</h3>
      <h3>{age}</h3>
      <button onClick={handleFetchData}>change name and age</button>
    </div>
  );
};
export default Person;

复制代码

Provider and Inject in Hook

Hook中也有类似的机制

父组件

import personStore from '../store/person'
export function App() {
    return (
      <Provider personStore={personStore} >
        <Person />
      </Provider>
    )
}
复制代码

person

import React, { useCallback, useContext } from 'react';
import { MobXProviderContext, useObserver } from 'mobx-react';
import { runInAction } from 'mobx';

const fetchData = () => new Promise(resolve => setTimeout(() => resolve(0), 500));

const Person: React.FC = () => {

  // 获取personStore
  const { personStore } = useContext(MobXProviderContext)

  const handleFetchData = useCallback(async () => {
    await fetchData();
    runInAction(() => {
      personStore.changeName('change name from data');
      personStore.changeAge(12);
    });
  }, []);

  const name = useObserver(() => 'the name is ' + personStore.name);
  const age = useObserver(() => 'the age is ' + personStore.age);

  return (
    <div>
      <h3>{name}</h3>
      <h3>{age}</h3>
      <button onClick={handleFetchData}>change name and age</button>
    </div>
  );
复制代码

异步

mobx hook中的异步操作依赖runInAction, 它会将你的store批量更新,下面是普通hook和mobx hook操作异步的比较

// 组件挂载之后,拉取数据并重新渲染。不考虑报错的情况
function AppWithHooks() {
    const [data, setData] = useState({})
    const [loading, setLoading] = useState(true)
    useEffect(async () => {
        const data = await fetchData()
        // 由于在异步回调中,无法触发批量更新,所以会导致 setData 更新一次,setLoading 更新一次
        setData(data)
        setLoading(false)
    }, [])
    return (/* ui */)
}

function AppWithMobx() {
    const store = useLocalStore(() => ({
        data: {},
        loading: true,
    }))
    useEffect(async () => {
        const data = await fetchData()
        runInAction(() => {
            // 这里借助 mobx 的 action,可以很好的做到批量更新,此时组件只会更新一次
            store.data = data
            store.loading = false
        })
    }, [])
    return useObserver(() => (/* ui */))
}
复制代码

最佳实践

永远不要在业务组件中直接修改store

请勿在业务中使用store.name = 'xxx'来修改组件值,请使用action方法来触发

可以开启useStrict来确保这一项

import { useStrict } from 'mobx'
useStrict(true)
复制代码

将rest api从action中剥离

import { observable, action, runInAction, makeObservable } from 'mobx';

// 假装是一个rest api
const queryPersonInfo = (): Promise<any> =>
  new Promise(resolve =>
    resolve({
      name: 'jenson',
      age: 21,
    }),
  );

class Person {
  constructor() {
    // mobx6.0后的版本都需要手动调用makeObservable(this),不然会发现数据变了视图不更新
    makeObservable(this);
  }
  @observable name = 'xxx';
  @observable age = 20;

  @action
  async queryPersonInfo() {
    const { name, age } = await queryPersonInfo();
    runInAction(() => {
      this.name = name;
      this.age = age;
    });
  }
}
const person = new Person();
export default person;
复制代码
import React, { useContext, useEffect } from 'react';
import { MobXProviderContext, useObserver } from 'mobx-react';
const Person: React.FC = () => {
  const { personStore } = useContext(MobXProviderContext);

  useEffect(() => {
    personStore.queryPersonInfo();
  }, []);

  return useObserver(() => (
    <div>
      {personStore.name} {personStore.age}
    </div>
  ));
};
export default Person;

复制代码
分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改