Introduction to the
Mobx is a simple, extensible state management library. React and Mobx are a powerful combination. React handles rendering applications and Mobx handles application state management, which React uses
Version Support
- The latest version is 6. Versions 4 and 5 are no longer supported
- Decorator syntax is not recommended in MobX 6 because it is not ES standard and the standardization process takes a long time, but it can still be enabled through configuration
- MobX can run in any ES5-enabled environment, including browsers and Nodes
- MobX is usually used with React, but it can also be used in Angular and Vue.
Introduction to Mobx
download
- Mobx: Mobx core library
- Mobx-react-lite: Only function components are supported
- Mox-react: Supports both function and class components
The core concept
- Observable State: State tracked by MobX.
- Action: A method that allows state modification. In strict mode, only the Action method is allowed to change state.
- Computed: Calculates values based on new values derived from application state
The working process
An introduction to case
Scenario description: Counter: Displays the value status in the component. Click the button to add the value by one. Click the button to reset the value.
// Counter Component -> Counter Component
// Observer: Monitors the Mobx-tracked Observable state used by the current component, notifying React to update the view when state changes
import { observer } from "mobx-react-lite"
function Counter({ counterStore }) {
return (
<div>
<p className="paragraph">{counterStore.count}</p>
<button onClick={()= >CounterStore. Increment ()} the className = "button" > 1</button>
<button onClick={()= >CounterStore. Reset ()} the className = "button" > reset</button>
</div>)}export default observer(Counter)
Copy the code
// Counter Store -> Store that manages Counter components
import { makeAutoObservable } from "mobx"
class CounterStore {
// Numeric state
count = 10
constructor() {
// Set the property in the parameter object to Observable state
// Set the method in the parameter object to action
makeAutoObservable(this)}// Increment the numeric state by one
increment() {
this.count += 1
}
// Reset the numeric state
reset() {
this.count = 0}}export default CounterStore
Copy the code
// App Component -> root Component
// Import the Counter component
import Counter from "./components/Counter/Counter"
// Import the Store that manages the Counter component
import CounterStore from "./stores/Counter/CounterStore"
// Create a Store instance object that manages the Counter component
const counterStore = new CounterStore()
function App() {
// Call the Counter component and pass in the Store that manages its state
return <Counter counterStore={counterStore} />
}
export default App
Copy the code
makeAutoObservable
// target: Sets the properties and methods in the target object to Observable state and Action
// overrides: sets some properties or methods in the target object to normal properties
// options: Configures the object, autoBind, so that the action method always has the correct this referencemakeAutoObservable(target, overrides? , options?) makeAutoObservable(this, {reset: false}, {autoBind: true})
Copy the code
conclusion
State changes are necessary to update views
- The state needs to be marked as
observable state
- Methods that change the state need to be marked as
action
methods - The component view must pass
observer
Methods the parcel
You can use the makeAutoObservable method to set object properties toObservable State and object methods to action methods
You can use the Observer method to monitor the Mobx-tracked Observable state used by the current component and notify React to update the view when the state changes
TodoList case
Create the initial TodoListStore
Create TodoListStore to manage to-do list status.
// TodoListStore.js
class TodoListStore {
todos = []
constructor(todos) {
if (todos) this.todos = todos
}
}
export default TodoListStore
Copy the code
Create a TodoViewStore to manage the status of to-do items.
// TodoViewStore.js
class TodoViewStore {
id = Math.random()
title = ""
completed = false
constructor(title) {
this.title = title
}
}
export default TodoViewStore
Copy the code
Create the TodoListStore instance object and pass it to the TodoListView component
// App.js
import TodoListView from "./components/Todos/TodoListView"
import TodoListStore from "./stores/Todos/TodoListStore"
import TodoStore from "./stores/Todos/TodoStore"
const counterStore = new CounterStore()
const todoListStore = new TodoListStore([
new TodoStore("Hello MobX"),
new TodoStore("Hello React")])function App() {
return <TodoListView TodoListStore={todoListStore} />
}
Copy the code
Render the initial backlog event
// TodoListView.js
function TodoListView({ TodoListStore }) {
return (
<ul className="todo-list">
{TodoListStore.todos.map(todo => (
<TodoView key={todo.id} todo={todo} />
))}
</ul>)}// TodoView.js
function TodoView({ todo }) {
return <label>{todo.title}</label>
}
Copy the code
Create a to-do list
- Create the createTodo method in the TodoListStore class to add todos to the toDOS array
// TodoListStore.js
import TodoStore from "./TodoStore"
class TodoListStore {
createTodo(title) {
this.todos.push(new TodoStore(title))
}
}
Copy the code
- Create the to-do item by calling the createTodo method in the TodoHeader component
// TodoListView.js
function TodoListView({ TodoListStore }) {
return <TodoHeader createTodo={title= > TodoListStore.createTodo(title)} />
}
Copy the code
// TodoHeader.js
import { useState } from "react"
function TodoHeader({ createTodo }) {
const [title, setTitle] = useState("")
return (
<header className="header">
<input
value={title}
onChange={event => setTitle(event.target.value)}
onKeyUp={event => {
if (event.key === "Enter") {
createTodo(title)
setTitle("")
}
}}
/>
</header>
)
}
Copy the code
- Set the conditions necessary for state changes to update the view
// TodoListStore.js
import { action, makeObservable, observable } from "mobx"
class TodoListStore {
constructor() {
makeObservable(this, {
todos: observable,
createTodo: action
})
}
}
Copy the code
// TodoListView.js
import { observer } from "mobx-react-lite"
function TodoListView (){}
export default observer(TodoListView)
Copy the code
Create Store context
We want to be able to get TodoListStore directly in each component, rather than passing it through the props property.
// TodoListStore.js
import { createContext, useContext } from "react"
const TodoListStoreContext = createContext()
const TodoListStoreProvider = ({ store, children }) => {
return (
<TodoListStoreContext.Provider value={store}>
{children}
</TodoListStoreContext.Provider>
)
}
const useTodoStore = () => {
return useContext(TodoListStoreContext)
}
export { TodoListStore, TodoListStoreProvider, useTodoStore }
Copy the code
// App.js
import { TodoListStore, TodoListStoreProvider } from "./stores/Todos/TodoListStore"
function App() {
return (
<TodoListStoreProvider store={todoListStore}>
<TodoListView/>
<Counter counterStore={counterStore} />
</TodoListStoreProvider>
)
}
Copy the code
// TodoListView.js import { useTodoListStore } from ".. /.. /stores/Todos/TodoListStore" function TodoListView() { const todoListStore = useTodoListStore() }Copy the code
// TodoHeader.js import { useTodoListStore } from ".. /.. /stores/Todos/TodoListStore" function TodoHeader() { const todoListStore = useTodoListStore() todoListStore.createTodo(title) }Copy the code
Delete to-do events
// TodoListStore.js
class TodoListStore {
constructor() {
makeObservable(this, {
removeTodo: action
})
}
removeTodo(id) {
const index = this.todos.findIndex(todo => todo.id === id)
this.todos.splice(index, 1)
}
}
Copy the code
// TodoView.js import { useTodoListStore } from ".. /.. /stores/Todos/TodoListStore" function TodoView({ todo }) { const todoListStore = useTodoListStore() return <button onClick={() => todoListStore.removeTodo(todo.id)} className="destroy" /> }Copy the code
Changing task Status
// TodoStore.js import { makeObservable, observable, action } from "mobx" class TodoStore { completed = false constructor() { makeObservable(this, { completed: observable, toggle: action }) } toggle() { this.completed = ! this.completed } }Copy the code
// TodoView.js
import { observer } from "mobx-react-lite"
function TodoView({ todo }) {
return (
<li className={todo.completed ? "completed" : ""}>
<input checked={todo.completed} onChange={() => todo.toggle()} className="toggle" type="checkbox" />
</li>
)
}
export default observer(TodoView)
Copy the code
Correct this to point to
<input onChange={() => todo.toggle()} /> <input onChange={todo.toggle}/> // this points to an errorCopy the code
makeObservable(this, {
toggle: action.bound
})
Copy the code
Count your to-do list
The backlog count is a derived state, that is, it depends on the existing state (TOdos).
Derived states can be implemented using calculated values, which are automatically updated when the dependent state changes.
// TodoListStore.js import { computed } from "mobx" class TodoListStore { constructor() { makeObservable(this, { unCompletedTodoCount: computed }) } get unCompletedTodoCount() { return this.todos.filter(todo => ! todo.completed).length } }Copy the code
// TodoFooter.js import { useTodoListStore } from ".. /.. /stores/Todos/TodoListStore" import { observer } from "mobx-react-lite" function TodoFooter() { const todoListStore = useTodoListStore() return <strong>{todoListStore.unCompletedTodoCount}</strong> item left } export default observer(TodoFooter)Copy the code
Note: Calculated values are cached.
get unCompletedTodoCount() { console.log("unCompletedTodoCount") return this.todos.filter(todo => ! todo.completed).length }Copy the code
{todoListStore.unCompletedTodoCount} {todoListStore.unCompletedTodoCount} {todoListStore.unCompletedTodoCount} // Computed properties are called multiple times, but console.log inside the method only prints once, indicating that computed properties are cached.Copy the code
To-do list filtering
// TodoListStore.js class TodoListStore { todos = [] filter = "all" constructor() { makeObservable(this, { filter: observable, changeFilter: action, filterTodos: computed }) } get filterTodos() { switch (this.filter) { case "all": return this.todos case "active": return this.todos.filter(todo => ! todo.completed) case "completed": return this.todos.filter(todo => todo.completed) default: return this.todos } } changeFilter(filter) { this.filter = filter } }Copy the code
// TodoFooter.js
function TodoFooter() {
const todoListStore = useTodoListStore()
return (
<footer className="footer">
<ul className="filters">
<li>
<button
onClick={() => todoListStore.changeFilter("all")}
className={todoListStore.filter === "all" ? "selected" : ""}
>
All
</button>
</li>
<li>
<button
onClick={() => todoListStore.changeFilter("active")}
className={todoListStore.filter === "active" ? "selected" : ""}
>
Active
</button>
</li>
<li>
<button
onClick={() => todoListStore.changeFilter("completed")}
className={todoListStore.filter === "completed" ? "selected" : ""}
>
Completed
</button>
</li>
</ul>
</footer>
)
}
Copy the code
// TodoListView.js
function TodoListView() {
const todoListStore = useTodoListStore()
return (
<ul className="todo-list">
{todoListStore.filterTodos.map(todo => <TodoView key={todo.id} todo={todo} />)}
</ul>
)
}
Copy the code
Load a remote task
- Download and start jSON-server
npm install -g json-server
json-server ./src/todo.json --port 3005
yarn add axios
{
"todos": [
{
"id": 1,
"title": "React",
"finished": false
},
{
"id": 2,
"title": "Angular",
"finished": false
},
{
"id": 3,
"title": "Vue",
"finished": false
}
]
}
Copy the code
- Create a loadTodos method to load the initial task
// TodoListStore.js
import axios from "axios"
import { runInAction } from "mobx"
class TodoListStore {
constructor(todos) {
this.loadTodos()
}
async loadTodos() {
let todos = await axios.get("http://localhost:3005/todos").then(response => response.data)
runInAction(() => todos.forEach(todo => this.todos.push(todo)))
}
}
Copy the code
- Solve the problem that the status of the remote loading to-do cannot be switched
The reason is that there is no toggle method on the prototype object of the task object that loads the to-do remotely.
import TodoStore from "./TodoStore"
class TodoListStore {
async loadTodos() {
runInAction(() => todos.forEach(todo => this.todos.push(new TodoStore(todo.title))))
}
}
Copy the code
Create RootStore
By creating a RootStore, you can merge CounterStore and TodoListStore, so that any state can be accessed from any component, facilitating global state sharing.
// RootStore.js
import CounterStore from "./Counter/CounterStore"
import TodoListStore from "./Todos/TodoListStore"
import { createContext, useContext } from "react"
class RootStore {
constructor() {
this.counterStore = new CounterStore()
this.todoListStore = new TodoListStore()
}
}
const RootStoreContext = createContext()
const RootStoreProvider = ({ store, children }) => {
return (
<RootStoreContext.Provider value={store}>
{children}
</RootStoreContext.Provider>
)
}
const useRootStore = () => {
return useContext(RootStoreContext)
}
export { RootStore, RootStoreProvider, useRootStore }
Copy the code
// App.js
import Counter from "./components/Counter/Counter"
import TodoListView from "./components/Todos/TodoListView"
import { RootStore, RootStoreProvider } from "./stores/RootStore"
const rootStore = new RootStore()
function App() {
return (
<RootStoreProvider store={rootStore}>
<TodoListView />
<Counter />
</RootStoreProvider>
)
}
export default App
Copy the code
Import {useRootStore} from ".. import {useRootStore} from ".. /.. /stores/RootStore" function TodoHeader() { const { todoListStore } = useRootStore() } function Counter() { const { counterStore } = useRootStore() }Copy the code
Data detection
Monitor data changes to perform side effects, and receive a function as a parameter to perform side effects. The function is run when observable state and computed changes are used inside the parameter function, and once when autorun is run initially.
import { autorun } from "mobx" import { useEffect } from "react" function Counter() { const { counterStore } = UseRootStore () useEffect(() => {// Ensure that the Autorun method is initialized only once Autorun (() => {console.log(counterStore.count)})}, [])}Copy the code
For basic data types, which are passed by value, MOBx can only trace the original property, not the copied value.
UseEffect (() => {let count = counterstore.count autorun(() => {console.log(count)})}, [])Copy the code
For reference data types, mobx can trace as long as the reference address does not change
// class CounterStore {person = {name: ""}}Copy the code
Function Counter() {const {counterStore} = useRootStore() useEffect() => {const person = counterStore.person autorun(() => { console.log(person.name) }) }, []) return ( <div> <p>{counterStore.person.name}</p> <button onClick={() => runInAction(() => (counterStore.person.name </ code > < code onClick={() => (counterStore. Person ={name: </button> </div>)}Copy the code
To monitor state changes to perform side effects, take two functions as arguments, the first function returns the state to monitor, the second function performs the side effects, and the second function executes only when the state returned by the first function changes. The reaction method provides state control of finer particle size.
Unlike Autorun, Reaction does not initially perform side effects.
import { reaction } from "mobx"
function Counter() {
useEffect(() => {
reaction(
() => counterStore.count,
(current, previous) => {
console.log(current)
console.log(previous)
}
)
}, [])
}
Copy the code