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

  1. Observable State: State tracked by MobX.
  2. Action: A method that allows state modification. In strict mode, only the Action method is allowed to change state.
  3. 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

  1. The state needs to be marked asobservable state
  2. Methods that change the state need to be marked asactionmethods
  3. The component view must passobserverMethods 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

  1. 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
  1. 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
  1. 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

  1. 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
  1. 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
  1. 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