preface
Now, react Hooks are working on this project. They are not comparing Class Componets and Function Component. Use gracefully and organize your front-end data flow
React hooks are supported in the current MOBx V16 version, which includes v17, V16.8, and FC. There are other great data management tools out there, too, like Recoil.
- Read the previous article: Mobx vs Recoil Application
Hopefully reading this article has helped you understand: How to use Mobx elegantly
reference
- mobx.js.org/
- mobx-react.js.org/
Environment to prepare
Install the latest versions of MOBx and Mox-React
npm install mobx mobx-react --save`
Copy the code
The basic use
Global MOBX configuration
- In store/index.ts (Store entry file), configure the mobx option
import { configure } from 'mobx';
// Global MOBx configuration
configure({
enforceActions: 'always'.// You always need action to change the state
computedRequiresReaction: true // Disallow direct access to any unobserved computed values from outside the action or reaction
});
Copy the code
Two ways to create a Store
- The makeAutoObservable automatically observes all data schemas and is recommended
- MakeObservable customizes observable properties
Automatically observe all data
- makeAutoObservable
import { makeAutoObservable, runInAction } from 'mobx';
class TestStore {
isReady = false; // observable state
amount = 1; // observable state
data; // observable state
constructor() {
// Automatically listen on all attributes
makeAutoObservable(this);
}
// computed
get price() :string {
return ` $The ${this.amount * 100}`;
}
// action
increment(): void {
this.amount++;
}
/ / async action,
async asyncAction(): Promise<void> {
try {
const data = await geAsyncData();
// In an asynchronous action, the code contains the runInAction(() => {/**... * /})
runInAction(() = > {
this.data = data;
this.isReady = true;
});
} catch (e) {
throw new Error(e); }}}export default new TestStore();
Copy the code
Custom observable properties
- Makeobservables, programs that control observable data themselves
Note that a store can only exist in one of makeAutoObservable or makeObservable
import { makeObservable, observable, runInAction } from 'mobx';
class TestStore {
isReady = false; // state
amount = 1; // state
data;
constructor() {
// Custom observable
makeObservable(this, {
isReady: observable,
amount: observable,
price: computed,
data: observable,
increment: action,
asyncAction: action
});
}
// computed
get price() :string {
return ` $The ${this.amount * 100}`;
}
// action, change the state, do not need return, mobx specification does not allow return
increment(): void {
this.amount++;
}
// async action, using the runInAction
async asyncAction(): Promise<void> {
try {
const data = await geAsyncData();
// In an asynchronous action, the code contains the runInAction(() => {/**... * /})
runInAction(() = > {
this.data = data;
this.isReady = true;
});
} catch (e) {
throw new Error(e); }}}export default new TestStore();
Copy the code
Use In React
- Mox-react needs to be combined to achieve store observable data changes, and reference store componet to update automatically
- Business components reference their own stores on demand, “plug and play”
import { FC } from 'react';
import { observer } from 'mobx-react';
import store from './store';
const Test: FC = () = > {
return (
<div>
<h1>About</h1>
<div>count from main: {store.amount}</div>
<div>price: {store.price}</div>
<button type="button" onClick={()= > store.increment()}>add +1</button>
</div>
);
};
/ / listening Component
export default observer(Test);
Copy the code
Advanced usage
State induction
The use of when is not recommended because the logic is confusing and there are better ways to use it later in the document
When and reaction should be defined in constructor and not in action, which may cause monitoring confusion
import { makeAutoObservable, reaction, when } from 'mobx';
class TestStore {
isReady = false;
count = 0;
constructor() {
makeAutoObservable(this);
// listen for isReady, and when isReady becomes true, doSomething (one-time behavior)
when(() = this.isReady, () = > this.doSomething());
// Listen for amount, and output value, previousValue after each amount change
reaction(
() = > this.amount,
(value, previousValue) = > {
console.log(value, previousValue); }); } doReady():void {
this.isReady = true;
}
doSomething(): void{... } increment():void {
this.amount++; }}export default new TestStore();
Copy the code
Multiple stores sense each other
It is not recommended to use it because the logic is easily confused. There are better ways to use it in the document
// userStore.ts
import { makeAutoObservable, runInAction } from 'mobx';
import * as userService from '@service/user.ts';
class UserStore {
userInfo = {};
isReady = false;
constructor() {
makeAutoObservable(this);
}
async init(): Promise<void> {
await this.getUserInfo();
}
async getUserInfo(): Promise<void> {
try {
const data = await userService.getUserData();
runInAction(() = > {
this.userInfo = data;
this.isReady = true;
});
} catch (e) {
throw new Error(e); }}}export default new UserStore();
Copy the code
// testStore.ts
import { makeAutoObservable, when } from 'mobx';
import userStore from './userStore.ts';
class TestStore {
amount = 1;
constructor() {
makeAutoObservable(this);
// When userStore.isReady becomes true, execute your init method immediately
when(
() = > userStore.isReady,
() = > this.init()
);
}
get price() :string {
return ` $The ${this.amount * 100}`;
}
init(): void {
console.log('testStore init... ')
}
increment(): void {
this.amount++; }}export default new TestStore();
Copy the code
Inject Public Store mode
- If there is shared data in the project and for multi-layer sharing (it is cumbersome to pass layers of props),
-
- The mobx-React Provider mode is no longer applicable to react hooks, which raise errors
-
- Use inject mode to establish the context by createContext -> useContext
// Take userStore as an example
class UserStore {/ * *... * /}
export const userStore = new UserStore();
Copy the code
// loadStores. Ts directly exports all stores
export { userStore } from './modules/userStore';
export { testStore } from './modules/testStore';
Copy the code
// store/index.ts
import { useContext, createContext } from 'react';
import * as stores from './loadStores';
// Create context
const storesContext = createContext(stores);
// react with store context
const useStores = (): any= > useContext(storesContext);
export { stores, useStores }
Copy the code
// commponets/Counter.ts
import { FC } from 'react';
import { observer } from 'mobx-react';
import { useStores } from '@store/index';
const Counter: FC = () = > {
// const { testStore } = useStores();
// Destruct the action. Note that the action cannot be destructed, which causes this problem
const { testStore: { amount, price } } = useStores();
return (
<div>
<! -- <div>{testStore.amount}</div> <div>price: {testStore.price}</div> -->
<div>{amount}</div>
<div>price: {price}</div>
<button type="button" onClick={()= > testStore.increment()}>add +1</button>
</div>
);
};
/ / listening Component
export default observer(Counter);
Copy the code
Store Scheduling Task
- A -> B -> C
- It is not recommended to use when (When may cause cross-reference between multiple stores and high maintenance cost).
- Call the task store by creating a system store
// store/index.ts
import { useContext, createContext } from 'react';
import { configure, makeAutoObservable, runInAction } from 'mobx';
import * as stores from './installStores';
// Global MOBx configuration
configure({
enforceActions: 'always'.// Action is always needed to change the state
computedRequiresReaction: true // Disallow direct access to any unobserved computed values from outside the action or reaction
});
/** * store data flow control */
class ScheduleStore {
isRunning = false;
constructor() {
makeAutoObservable(this);
}
// The scheduling task starts
async run(): Promise<void> {
try {
/** ---- System initialization data -- */
// User information is initialized
await stores.userStore.init();
// test Information initialization
await stores.testStore.init();
// ...
// The data is ready to render the page
runInAction(() = > {
this.isRunning = true;
});
} catch (e) {
console.log(e); }}}const storesContext = createContext(stores);
const scheduleContext = createContext({
schedule: new ScheduleStore()
});
const useStores = (): any= > useContext(storesContext);
const useSchedule = (): any= > useContext(scheduleContext);
export { stores, useStores, useSchedule };
Copy the code
// App.ts
import { FC, useEffect, useContext, useState } from 'react';
import { observer } from 'mobx-react';
import { useSchedule } from '@store/index';
const App: FC<RouteComponentProps> = () = > {
const { schedule } = useSchedule();
useEffect(() = > {
if(! schedule.isRunning) {console.log('schedule to run');
schedule.run();
} else {
console.log('init gar');
// doSomething
}
}, [schedule]);
return schedule.isRunning ? <Home /> : <Loadding />
}
export default observer(App);
Copy the code
mobx instantiation instead of react hooks fetch data
- React hooks request initial data in a business page, typically implemented this way
useEffect(() = > {
store.getAsynData();
}, [])
Copy the code
- The instantiation of Mobx Store is used to initialize the store data
- UseEffect [() => {}, []]
// productionStore.ts
import { makeAutoObservable, runInAction } from 'mobx';
class ProductionStore {
list = [];
constructor() {
makeAutoObservable(this);
this.init();
}
async init(): Promise<void> {
await this.getList();
}
async getList(): Promise<void> {
try {
const data = await getAsyncList();
runInAction(() = > {
this.list = data;
});
} catch (e) {
throw new Error(e); }}}export default new ProductionStore();
Copy the code
Instantiate store externally
- Business page, export Class directly, instantiate externally (passable)
1. Define a Store Class
export default class ProductStore {
detailInfo = {};
constructor(id: string) {
makeAutoObservable(this);
this.init(id);
}
async init(id: string) :Promise<void> {
await this.getProductDetail();
}
async getProductDetail(id: string) :Promise<void> {
try {
const data = await getProDetail(id);
runInAction(() = > {
this.detailInfo = data;
});
} catch (e) {
throw new Error(e); }}}Copy the code
2. Instantiate externally
import ProductStore from './ProductStore';
const ProDetail: FC<RouteComponentProps> = ({ id }) = > {
const productStore = new ProductStore(id);
// Kakadi, which is dry...
return(a); }Copy the code
Multiple instance stores coexist
- Business pages generally do not involve multi-instance coexistence. Here, tabsStore multi-tab card coexistence is taken as an example
type TabType = {
key: string; title? :string;
};
/ / the singleton TAB
class Tab {
active = false;
key: string; / / the primary key
title: string;
constructor(options: TabType) {
makeAutoObservable(this);
this.init(options);
}
init(options): void {
Object.assign(this, options);
}
setActive(val: boolean) :void {
if(val ! = =this.active) {
this.active = val; }}}/ / tabs management
class Tabse {
tabList = [];
constructor() {
makeAutoObservable(this);
}
get activeTabKey() :string {
const activeTab = this.tabList.find(x= > x.active);
if (activeTab) {
return activeTab.key;
}
return this.tabList.length ? this.tabList[0].key : undefined;
}
get activeTab() {
return this.tabList.find(x= > x.active);
}
/** * add a TAB * if it exists, create a new TAB * if it does not exist@param tab
*/
addTabIfNotExist = (tab: TabType): void= > {
const { key } = tab;
// Check whether it exists
let target = this.tabList.find(x= > x.key === key);
// Activate the route
history.push(tab.key);
if(! target) { target =new Tab(tab);
this.tabList.push(target);
}
/ / switch TAB
this.switchTab(target, false);
}
/** * Add a TAB page to close the current page *@param tab
*/
addTabRemoveCurrent = tab= > {
const activeTab = this.activeTab.key;
this.addTabIfNotExist(tab);
this.removeTab(activeTab, true);
}
/** * switch TAB *@param tab
* @param pushHistory
*/
switchTab(tab, pushHistory: boolean) :void {
if(! tab? .active) {// Activate the target value after disabling the current active state
this.tabList.find(x= > x.active).active = false;
tab.active = true;
if (pushHistory) {
history.push(tab.key)
}
if(location.pathname ! = ='/' + tab.key) {
const url = location.origin + '/' + tab.ke
window.history.pushState({}, 0, url); }}}/** * Close TAB *@param key
* @param state
*/
removeTab(key, ): void {
// Cannot be closed when there is only one TAB
if (this.tabList.length === 1) {
return;
}
/ /...}}export const tabsStore = new Tabse();
Copy the code
The end of the
Basic usage and advanced usage of Mobx are basically over here, I believe that by reading this article, you can better organize and manage the data of the front-end project
Join us at ❤️
Bytedance Xinfoli team
Nice Leader: Senior technical expert, well-known gold digger columnist, founder of Flutter Chinese community, founder of Flutter Chinese community open source project, well-known developer of Github community, author of dio, FLY, dsBridge and other well-known open source projects
We look forward to your joining us to change our lives with technology!!
Recruitment link: job.toutiao.com/s/JHjRX8B