上一篇文章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的魅力:
固然,本文只是一个开胃菜,还有更多优雅的特性等待后面的文章中慢慢去挖掘。
喜欢本文欢迎关注和收藏,转载请注明出处,谢谢支持。