前言
之前项目中状态管理清一色的都是用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;