來點不一樣的狀態管理 mobx

其實比 React 簡單很多。

簡述

最近因為工作的關係,得學習新的工具。

以 React 來舉例的話,講到狀態管理大家第一個想到的應該是 redux,不過前輩建議我們要多學新的東西,所以這次的專案不用 redux 而是 mobx。

我覺得這樣還蠻好的,雖然要花時間學新東西,不過也蠻有趣的。

簡單來說,mobx 也是一個狀態管理的工具,不過他比 redux 更簡單一些,也不需要太多的前置作業(boilerplate),只要掌握 actionobservecomputed 幾個觀念就差不多了。

四大要素

  1. store(class)儲存所有資訊的地方
  2. observale 要觀察的 state
  3. action 用來更新 state 的 function
  4. 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";

// 先留空,等會兒要把 store 給當作 props 傳進來
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 的藍圖,然後我們要做下面這幾件事:

  1. 建立 todos 內容(state)
  2. 讓 todos 變成「observable」(可觀察的)
  3. 建立 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 的屬性
// 這個屬性就是給其他人存取的 state
todos: TodoItem[] = []

// 這邊會用 makeObservable 來初始化
// 用途是告訴 mobx 他要觀察哪些 state
// 還有我們有哪些 action 跟 computed(這個後面會再介紹)
constructor() {
makeObservable(this, {
todos: observable,
addTodo: action
})
}

// action,說穿了就是一個 function 而已
addTodo(name: string): void {
const item: TodoItem = {
id: new Date().getTime(),
name,
completed: false
}
this.todos.push(item);
}
}

看完上面的 code 你應該就大概知道 action, makeObservable, observable 的用途是什麼了。

這邊只補充一下 makeObservable,他會接收兩個參數:

  1. store 的 instance(this
  2. 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 的用途是拿來給 typescript 用的
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);
}
}

// 把 instance 輸出
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';
// 引入 store
import { TodoStore } from './TodoStore';


const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
// 當作 props 傳下去
<TodoList todoStore={TodoStore} />
);

最後再回來調整一下 TodoList 的部分,既然現在已經拿到 store 了,就代表它可以透過 store 來存取 todosaddTodo 這兩個東西。

所以可以改成這樣:

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";

// 幫 props 設定 type
interface Props {
todoStore: TodoStoreImpl
}

// 透過 props 拿到 todoStore
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
}

// 不太懂的話可以想成這樣:observer(<TodoList />)
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>
)
})

做到這邊以後,畫面應該就會自動更新了:

refresh

接著請你自己再做一個 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 關鍵字來宣告 computed
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();

finish

做到這邊後,其實就差不多把 mobx 的基礎學完了,恭喜恭喜!

如果你想參考原始碼的話可以到這邊

重溫 React router dom 利用 Axios 來封裝 API
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×