其實比 React 簡單很多。
簡述
最近因為工作的關係,得學習新的工具。
以 React 來舉例的話,講到狀態管理大家第一個想到的應該是 redux,不過前輩建議我們要多學新的東西,所以這次的專案不用 redux 而是 mobx。
我覺得這樣還蠻好的,雖然要花時間學新東西,不過也蠻有趣的。
簡單來說,mobx 也是一個狀態管理的工具,不過他比 redux 更簡單一些,也不需要太多的前置作業(boilerplate),只要掌握 action
、observe
和 computed
幾個觀念就差不多了。
四大要素
- store(class)儲存所有資訊的地方
- observale 要觀察的 state
- action 用來更新 state 的 function
- computed 告訴你 store 裡面的資訊(類似 derived state 的概念)
來一個簡單的 Todo list
凡事從 todo list 開始都會吸收蠻快的,所以這邊一樣會做一個來當示範。
建立基本元件
這邊為了方便起見,不會把元件拆的太細,只會建立一個 TodoList
,大概長這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import React, { useState } from "react";
interface Props {
}
export const TodoList: React.FC<Props> = (props) => { const [value, setValue] = useState<string>(""); return ( <div> <form onSubmit={e => { e.preventDefault(); }}> <input type="text" value={value} onChange={e => setValue(e.target.value)} /> <button type="submit">submit</button> </form> </div> ) }
|
建立 store
接著要來建立 store 的部分。
在那之前先解釋一下,mobx 跟 redux 最大的不同不需要一大堆前置作業,如果專案不大的話,其實一個檔案就蠻夠用了。
首先要建立一個 class
,這個 class
就代表 store 的藍圖,然後我們要做下面這幾件事:
- 建立 todos 內容(state)
- 讓 todos 變成「observable」(可觀察的)
- 建立 action(修改 todos)
大致上是這樣,剩下的部分用 code 來說明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import { action, makeObservable, observable } from "mobx"
interface TodoItem { id: number, name: string, completed: boolean }
class TodoStoreImpl { todos: TodoItem[] = []
constructor() { makeObservable(this, { todos: observable, addTodo: action }) }
addTodo(name: string): void { const item: TodoItem = { id: new Date().getTime(), name, completed: false } this.todos.push(item); } }
|
看完上面的 code 你應該就大概知道 action
, makeObservable
, observable
的用途是什麼了。
這邊只補充一下 makeObservable
,他會接收兩個參數:
- store 的 instance(
this
)
- store 裡的所有 property
此外,應該有注意到在 addTodo
時竟然可以用「mutable」的方式來更新 state?
這個是 mobx 的一個特點,讓你可以用 mutable 的方式來更新 state,就跟 redux-tool-kit 的概念有點像,應該是會在背後幫你做一些處理(應該啦,我沒深究)。
最後,因為所有的 Component 會共用同一個 instance
,所以我們真正要 export 出去的會是 new 出來的東西,而不是 class
本身。
所以這邊要 new
一個 instance 出來,再把他 export 出去:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import { action, makeObservable, observable } from "mobx"
interface TodoItem { id: number, name: string, completed: boolean }
export class TodoStoreImpl { todos: TodoItem[] = []
constructor() { makeObservable(this, { todos: observable, addTodo: action }) }
addTodo(name: string): void { const item: TodoItem = { id: new Date().getTime(), name, completed: false } this.todos.push(item); } }
export const TodoStore = new TodoStoreImpl();
|
接著回到 index.js
的部分,它要做的事情只有一個,就是「把 store 引入,然後傳給 TodoList
」:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import TodoList from './TodoList';
import { TodoStore } from './TodoStore';
const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement ); root.render( <TodoList todoStore={TodoStore} /> );
|
最後再回來調整一下 TodoList
的部分,既然現在已經拿到 store 了,就代表它可以透過 store 來存取 todos
跟 addTodo
這兩個東西。
所以可以改成這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| import React, { useState } from "react"; import { TodoStoreImpl } from "./TodoStore";
interface Props { todoStore: TodoStoreImpl }
export const TodoList: React.FC<Props> = ({ todoStore }) => { const [value, setValue] = useState<string>(""); return ( <div> <form onSubmit={e => { e.preventDefault(); if (value) { // 透過 store 呼叫 addTodo todoStore.addTodo(value); setValue(""); } }}> <input type="text" value={value} onChange={e => setValue(e.target.value)} /> <button type="submit">submit</button> </form> {/* 透過 store 拿到 state 的資訊 */} <ul> {todoStore.todos.map(todo => <li key={todo.id}>{todo.name}</li>)} </ul> </div> ) }
|
關於 observer 和 HOC
做到這邊,你應該就可以新增 todo,然後透過 action 去改變 store 裡面的 todos
。可是你會發現一個問題,那就是畫面不會更新。
附註:雖然我實作的結果其實是會更新,但不確定是不是 mobx 後來有做什麼更新的原因,總之還是避免這樣寫比較好。
為什麼?
這是因為當我們新增 todo 時,會改變的是 store 裡的 property,而不是 todoStore
這個 props(instance)。
既然 props 沒有變,Component 自然就不會 re-render,畫面也當然不會更新。
所以呢,除了把 todoStore
傳給 Component 以外,還要幫 Component 用一層 observale
包住,也就是 HOC(Higher Order Component)的手法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import { observer } from "mobx-react-lite"; import React, { useState } from "react"; import { TodoStoreImpl } from "./TodoStore";
interface Props { todoStore: TodoStoreImpl }
export const TodoList: React.FC<Props> = observer(({ todoStore }) => { const [value, setValue] = useState<string>(""); return ( <div> <form onSubmit={e => { e.preventDefault(); if (value) { todoStore.addTodo(value); setValue(""); } }}> <input type="text" value={value} onChange={e => setValue(e.target.value)} /> <button type="submit">submit</button> </form>
<ul> {todoStore.todos.map(todo => <li key={todo.id}>{todo.name}</li>)} </ul> </div> ) })
|
做到這邊以後,畫面應該就會自動更新了:
接著請你自己再做一個 toggleTodo
的功能,並且畫面上顯示已完成 / 未完成的狀態(概念都差不多,所以這邊就不示範了)。
computed
最後要介紹的是 computed,顧名思義是用來做「計算」的東西。在這個例子裡,我們可以拿它來計算 todo 的已完成 / 未完成數量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| import { action, computed, makeObservable, observable } from "mobx";
interface TodoItem { id: number; name: string; completed: boolean; }
export class TodoStoreImpl { todos: TodoItem[] = [];
constructor() { makeObservable(this, { todos: observable, addTodo: action, toggleTodo: action, states: computed }); }
addTodo(name: string): void { const item: TodoItem = { id: new Date().getTime(), name, completed: false }; this.todos.push(item); }
toggleTodo(id: number): void { const index = this.todos.findIndex((item) => item.id === id); if (index > -1) { this.todos[index].completed = !this.todos[index].completed; } }
get states() { let completed: number = 0; let remaining: number = 0; this.todos.forEach((item) => { if (item.completed) { completed++; } else { remaining++; } }); return { completed, remaining }; } }
export const TodoStore = new TodoStoreImpl();
|
做到這邊後,其實就差不多把 mobx 的基礎學完了,恭喜恭喜!
如果你想參考原始碼的話可以到這邊。