上一篇文章MST基礎中簡單地介紹了MST的幾個基本概念和相關API,本文將帶你們搭配React實現一個TodoList。css
爲了省去枯燥的項目搭建過程,本文選擇使用stackblitz
平臺來編輯咱們的代碼。react
同窗們能夠點擊上面的地址fork一個starter項目,項目中已經配置好MST以及React相關的依賴,而且包含了一個簡單的Counter demo,後面將在這個starter的基礎上進行開發。mvc
從上面的地址進入後,你會獲得一個包含如下目錄結構的初始項目。less
其中,目錄components
用於存放React組件,目錄models
用於存放MST Model。dom
整個應用的Root Model在models/index.ts
文件中定義:ide
import { types } from 'mobx-state-tree';
import { Counter } from './Counter';
export const Root = types
.model('Root', {
counter: types.optional(Counter, {}),
});
複製代碼
定義好的Root Model會在項目的入口index.tsx
文件中被引入,並建立實例對象,而後使用mobx-react
提供的Provider
組件將Root Model的實例對象傳遞到應用的Context
中:post
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'mobx-react';
import { Root } from './models';
import { ModelInjector } from './components/ModelInjector';
import './style.css';
import { Counter } from './components/Counter';
const root = Root.create({});
const ConnectedCounter = () => (
<ModelInjector>
{(root) => <Counter model={root.counter}/>}
</ModelInjector>
);
function App () {
return (
<Provider root={root}>
<ConnectedCounter/>
</Provider>
);
}
render(<App />, document.getElementById('root'));
複製代碼
項目提供了一個名爲ModelInjector
的組件,index.tsx
代碼中,使用ModelInjector
組件對Counter
組件進行了一個包裝,將root.counter
這個節點Model做爲props傳給了Counter
組件。在本文以及後續的文章中,將會沿用這樣的方式在組件與Model之間創建鏈接。性能
這樣作的好處是,可讓TypeScript的靜態類型約束覆蓋到整個應用,開發過程當中能夠享受到類型帶來的便利:優化
ModelInjector
組件的實現比較簡單,能夠在項目中自行查看。
在開始動手編碼以前,必須明確要作的這個東西是什麼樣的。
咱們要作的這款TodoList你們應該比較熟悉:
這款TodoList來自TodoMVC。
因爲本文的主題不在UI的實現上,咱們能夠複用他的DOM結構和CSS,這會省去很多功夫。
明確要作什麼以後,就能夠着手開始分析這個應用的狀態結構了。
從最基礎的開始。TodoList的基本單位就是TodoItem,TodoItem具有的屬性是他的id
(用於編輯、刪除時進行跟蹤)、title
,以及是否完成的標識done
,因此能夠得出:
// models/TodoItem.ts
import { types, Instance } from 'mobx-state-tree';
export const TodoItem = types
.model('TodoItem', {
id: types.string,
title: types.string,
done: types.boolean,
})
.actions(self => ({
switchDone(done?: boolean) {
if (typeof done === 'boolean') {
self.done = done;
} else {
self.done = !self.done;
}
}
}));
export type TodoItemInstance = Instance<typeof TodoItem>;
複製代碼
新建models/TodoItem.ts
文件,寫入上面的代碼。
細心的同窗會發現,上面代碼中還export了一個type定義TodoItemInstance
。這個type表示的是TodoItem
這個Model的實例類型,能夠在定義React組件的props類型時使用。
應用還須要一個輸入框,在新增的時候輸入新TodoItem的title;以及一個可隱藏的輸入框用來編輯已有TodoItem的title。
這兩個輸入框的功能類似,都是維護輸入框的值並處理值的更新。不一樣的是編輯輸入框
會與某一個TodoItem關聯,而新增輸入框
沒有關聯對象。
可使用一個TodoForm
的Model來維護兩個輸入框的狀態:
// models/TodoForm.ts
import { types, Instance } from 'mobx-state-tree';
import { TodoItemInstance } from './TodoItem';
export const TodoForm = types
.model('TodoForm', {
value: types.optional(types.string, ''),
targetTodoId: types.optional(types.maybeNull(types.string), null),
})
.views(self => ({
get trimedValue () {
return self.value.trim();
},
get valid() {
return this.trimedValue.length > 0;
}
}))
.actions(self => ({
setTarget(target: TodoItemInstance) {
self.value = target.title;
self.targetTodoId = target.id;
},
update(value: string) {
self.value = value;
},
reset() {
self.value = '';
self.targetTodoId = null;
}
}));
export type TodoFormInstance = Instance<typeof TodoForm>;
複製代碼
TodoForm
中,使用value
維護輸入框的值,targetTodoId
表示當前關聯的TodoItem
的id。並提供了用於關聯TodoItem
的setTarget
方法,更新值的update
方法以及重置狀態的reset
方法。
這裏使用了types.optional
爲狀態設置了初始值。
另外還提供了兩個計算值:trimedValue
以及valid
。
這裏須要注意的是,在valid
的定義中,trimedValue
引用的是this
而不是self
,這是因爲valid
以及trimedValue
二者的定義寫在同一個views
方法中,在views
方法結束前,TypeScript的類型系統並不能觀察到self
對應的類型中包含valid
或者trimedValue
,因此須要使用this
來代替self
。
除了views
以外,actions
或者volatile
也須要注意上面這個問題。
完成上面的兩個Model以後,剩下的都是一些與列表相關的狀態了。將TodoItem與TodoForm進行組合,構成整個TodoList應用的基本Model:
// models/TodoList.ts
import { types } from 'mobx-state-tree';
import { TodoItem } from './TodoItem';
import { TodoForm } from './TodoForm';
export const TodoList = types
.model('TodoList', {
adderForm: types.optional(TodoForm, {}),
editorForm: types.optional(TodoForm, {}),
list: types.array(TodoItem),
});
複製代碼
其中adderForm
與editorForm
分別表示新增Todo
與編輯Todo
的表單Model,list
用於管理Todo列表。
仔細觀察目標的成品圖,他還包括三個篩選按鈕All
、Active
、Completed
,用於篩選展示的Todo列表的類型,這裏能夠將三種類型定義爲枚舉TodoFilterType
,新建enums.ts
文件,輸入代碼:
// enums.ts
export enum TodoFilterType {
All = 'All',
Active = 'Active',
Completed = 'Completed'
}
複製代碼
而後爲TodoList
新增一個filterType
的狀態:
// models/TodoList.ts
import { types } from 'mobx-state-tree';
import { TodoItem } from './TodoItem';
import { TodoForm } from './TodoForm';
import { TodoFilterType } from '../enums';
export const TodoList = types
.model('TodoList', {
adderForm: types.optional(TodoForm, {}),
editorForm: types.optional(TodoForm, {}),
list: types.array(TodoItem),
filterType: types.optional(types.string, TodoFilterType.All),
});
複製代碼
有了這幾個基礎狀態,就能夠獲得其餘幾個衍生狀態:
// models/TodoList.ts
...
export const TodoList = types
.model('TodoList', {
...
})
.views(self => ({
// 已完成的Todo列表
get doneList() {
return self.list.filter(i => i.done);
},
// 未完成的Todo列表
get activeList() {
return self.list.filter(i => !i.done);
},
// 是否所有完成
get isAllDone() {
return this.doneList.length === self.list.length;
},
// 剩餘未完成的Todo數量
get activeCount() {
return this.activeList.length;
},
// 當前展示的Todo列表
get showList() {
switch (self.filterType) {
case TodoFilterType.Active:
return this.activeList;
case TodoFilterType.Completed:
return this.doneList;
default:
return self.list;
}
},
// 是否顯示主體UI(沒有Todo數據的時候只顯示一個新增輸入框)
get isShowMain() {
return self.list.length > 0;
},
// 是否包含已完成的Todo,用於控制右下角[Clear completed]按鈕的展示和隱藏
get hasDoneTodos() {
return this.doneList.length > 0;
}
}))
複製代碼
最後,再補上更新狀態的actions:
// models/TodoList.ts
import { types, cast, Instance } from 'mobx-state-tree';
import uuid from 'uuid/v1';
...
export const TodoList = types
.model('TodoList', {
...
})
.views(self => ({
...
})
.actions(self => ({
// 切換所有完成/所有未完成
switchAllDone(done?: boolean) {
if (typeof done !== 'boolean') {
done = !self.isAllDone;
}
self.list.forEach(item => {
item.switchDone(done);
});
},
// 切換列表過濾類型
setFilterType(filterType: TodoFilterType) {
self.filterType = filterType;
},
// 新增Todo
addTodo() {
if (self.adderForm.valid) {
self.list.push(cast({
id: uuid(),
title: self.adderForm.trimedValue,
done: false
}));
self.adderForm.reset();
}
},
// 更新Todo
updateTodo() {
if (self.editorForm.valid) {
const item = self.list.find(i => i.id === self.editorForm.targetTodoId);
if (item) {
item.title = self.editorForm.trimedValue;
}
self.editorForm.reset();
}
},
// 刪除Todo
removeTodo(todoId: string) {
const index = self.list.findIndex(i => i.id === todoId);
if (index >= 0) {
self.list.splice(index, 1);
}
},
// 清除已完成Todos
clearDone() {
self.list = cast(self.list.filter(i => !i.done));
}
}));
export type TodoListInstance = Instance<typeof TodoList>;
複製代碼
上面的代碼在給一些狀態賦值的時候,用到了MST提供的cast
方法,這個方法僅在TypeScript中有意義,由於他僅僅是將入參的類型轉換成對應的狀態的類型,使得代碼的類型能經過TypeScript的檢測(由於在TypeScript看來,沒有cast的時候,等號左側和右側的兩個值並非類型匹配的)。
另外,在新增Todo的時候,使用了uuid
庫提供的方法生成Todo的惟一id。注意在項目中安裝uuid
依賴。
本實例中還依賴了classnames
庫,也須要一併安裝。因爲uuid
以及classnames
庫都不包含類型定義文件(*.d.ts),在項目中新增了一個modules.d.ts
文件,代碼以下:
// modules.d.ts
declare module 'classnames';
declare module 'uuid/v1';
複製代碼
要在應用中使用上面定義的Model,還須要將他們加入到狀態樹中,更新models/index.ts
文件:
// models/index.ts
import { types } from 'mobx-state-tree';
import { TodoList } from './TodoList';
export const Root = types
.model('Root', {
todoList: types.optional(TodoList, {}),
});
複製代碼
至此,這個TodoList實例的狀態樹就構造完成了。
UI方面並非本系列文章的重點,而且本文TodoList的UI實現比較簡單,套用了TodoMVC的DOM結構和CSS,本文中只對幾個關鍵的點作一下說明,完整的代碼見文末。
使用mobx-react
包提供的observer
裝飾器裝飾後的組件能響應observable的變化,並作了諸多的性能優化,儘量爲你的組件加上observer,除非你想要自定義shouldComponentUpdate
來控制組件更新時機。
有的同窗可能看到過相似這樣的說法:
用Redux管理全局狀態,用組件State管理局部狀態。
筆者不認同這種說法,根據筆者的經驗來看,當項目複雜到必定的程度,使用State管理的狀態會難受到讓你抓狂:某個深層次的組件State只能經過改變上層組件傳遞的props來進行更新。
更況且,如今無狀態(state less)組件愈來愈受到你們的承認,react hooks的出現也順應了組件無狀態的這個發展趨勢。
當應用都由無狀態組件構成,應用的狀態都存儲在觸手可及的地方(如Redux或MST),想要在某些時刻修改某個狀態值就變得垂手可得。
這也是上文中,將輸入框的值維護在TodoForm
中的一個重要緣由。
本文使用MST搭配React構建了一個完整的TodoList應用,不知道同窗們有沒有體會到MST的魅力:
固然,本文只是一個開胃菜,還有更多優雅的特性等待後面的文章中慢慢去挖掘。
喜歡本文歡迎關注和收藏,轉載請註明出處,謝謝支持。