面向 React/Redux 开发者的 MobX 4 简介
示例 A1:React + MobX
示例 A2:那又怎样?
示例 A3:但这不是 OO
示例 B1:但是我讨厌螺旋桨钻孔!
示例 B2:好的,但是异步怎么样
概念突破!回顾一下!
示例 E1:MobX 开发工具
敬请关注...
MobX 利用可观察对象的“魔力”来管理状态和副作用。这不仅学习起来比较费劲,而且是一种完全不同的编程范式。目前关于如何将 React 与 Mobx 结合使用的最新培训材料并不多,而关于将 React 与 Redux 结合使用的内容却非常多。
在本介绍中,我们将逐步构建一个简单的应用程序,ping 一个模拟 API,以了解 MobX 如何与 React 协同工作,然后制作一个 MobX + React 看板来展示 MobX 的强大功能!
我们将如何进行:
- 示例 A。构建一个基本应用,允许输入文本并显示在Display中。我们展示了如何建立s 和组件的基础知识。
observable
observer
- 示例 B 。我们将输入和显示拆分为兄弟组件,以模拟更复杂的应用。我们还通过 ping 模拟 API 引入了异步状态更新。为此,我们使用
mobx-react
Provider
将 MobX 状态放入 React 上下文中,以演示类似于 的兄弟组件间或兄弟组件间父组件间的简单通信react-redux
。 - 示例 C :我们向应用添加了辅助显示。演示了变量(Mobx 概念)的实用性。
computed
- 示例 D:我们将应用扩展至任意数量的显示。演示了如何使用数组和映射来实现 MobX 状态。
- 示例 E:调整和清理!我们添加了 MobX 开发工具,将整个应用程序置于模式中,并解释了 MobX s 和s
useStrict
的正式用法,以提高应用程序性能。action
transaction
本教程将使用最近发布的 MobX 4和 MobX-React 5。很多人将 MobX 与装饰器联系在一起,而装饰器目前还处于第二阶段的提案阶段。这(理所当然地)会让一些人犹豫不决,但 MobX 4 引入了基于非装饰器的语法,让我们不再有这样的借口!然而,对于教程作者来说,这是一个问题,因为你必须决定是教授其中一种,还是两种都教授。为了解决这个问题,本教程中的每个示例都将使用非装饰器语法作为主要版本,但会提供一个使用装饰器的副本来展示等效的实现(例如,示例 A 与装饰器 A)。
读者须知:本文并非推荐 MobX 优于 Redux,反之亦然。本文旨在为像我这样只熟悉 Redux 的读者,如实地介绍 MobX 的核心概念。我会尝试得出一些结论,但理性的人可能会不同意。此外,Michel Weststrate 也曾多次强调,这两个库所针对的需求和价值完全不同。
示例 A1:React + MobX
这是我们使用 React + MobX 的最基本的应用程序:
import { decorate, observable } from "mobx";
import { observer } from "mobx-react";
const App = observer(
class App extends React.Component {
text = ""; // observable state
render() {
// reaction
return (
<div>
Display: {this.text} <br />
<input
type="text"
onChange={e => {
this.text = e.target.value; // action
}}
/>
</div>
);
}
}
);
decorate(App, { text: observable });
您可以在此处看到observer
连接可观察text
属性,App
以便每当您更新时它都会重新渲染text
。
虽然这很好,但它实际上与使用state
和没有什么不同setState
。如果你有 React,你不需要 MobX 来实现这一点。
示例 A2:那又怎样?
让我们尝试分离状态和视图模型的关注点:
// this deals with state
const appState = observable({
text: "" // observable state
});
appState.onChange = function(e) { // action
appState.text = e.target.value;
};
// this deals with view
const App = observer(
class App extends React.Component {
render() { // reaction
const { text, onChange } = this.props.store;
return (
<div>
Display: {text} <br />
<input type="text" onChange={onChange} />
</div>
);
}
}
);
// you only connect state and view later on...
// ...
<App store={appState} />
这里是store
:
- 明确地作为 prop 传入(我们
Provider
稍后会使用该模式) - 自带动作处理程序(无需导入单独的 Reducer)
示例 A3:但这不是 OO
看一下上面代码的这部分。
const appState = observable({
text: "" // observable state
});
appState.onChange = function(e) { // action
appState.text = e.target.value;
};
是的,我不喜欢这样。该方法没有封装在可观察对象中。我们可以让它更面向对象吗?
// import { decorate } from 'mobx'
class State {
text = ""; // observable state
onChange = e => (this.text = e.target.value); // action
};
decorate(State, { text: observable });
const appState = new State()
啊,好多了(尤其是装饰器示例,您不需要使用decorate
)!
示例 B1:但是我讨厌螺旋桨钻孔!
就像react-redux
可以将商店放入 一样Provider
,它mobx-react
也有一个Provider
以相同方式工作的 。我们将 Display 和 Input 组件重构为兄弟应用程序:
import { inject, observer, Provider } from "mobx-react";
class State {
text = ""; // observable state
onChange = e => (this.text = e.target.value); // action
}
decorate(State, { text: observable });
const appState = new State();
const Display = inject(["store"])(
observer(({ store }) => <div>Display: {store.text}</div>)
);
const Input = inject(["store"])(
observer(
class Input extends React.Component {
render() {
// reaction
return <input type="text" onChange={this.props.store.onChange} />;
}
}
)
);
// look ma, no props
const App = () => (
<React.Fragment>
<Display />
<Input />
</React.Fragment>
);
// connecting state with context with a Provider later on...
// ...
<Provider store={appState}>
<App />
</Provider>
请注意,如果我要添加第二个商店,我只需定义另一个observable
,并将其作为Provider
另一个 prop 传递给它,然后我就可以从任何子级调用它。不再是 redux 风格了combineReducers
!
使用提供程序还有助于避免创建全局存储实例,这是MobX React 最佳实践中强烈建议不要做的事情。
MobX 4 注意:如果您只是尝试使用旧的 MobXobserver(['store'])
简写(它始终与observer
+同义inject(['store'])
),您将收到一个非常好的弃用警告,告诉您不要再这样做了。
我发现这个注入/观察器语法有点繁琐,所以这是一个很好的小实用函数,你可以定义它来输入更少的内容:
const connect = str => Comp => inject([str])(observer(Comp));
connect
嘿!这就像我们来自 的好朋友react-redux
!API 略有不同,但你可以随意定义🤷🏼♂️。
示例 B2:好的,但是异步怎么样
对于异步 API 获取,我们有几个选择。我们可以选择:
mobx-thunk
mobx-observable
mobx-saga
- 以及大约 300 个其他选项。
它们都是特别的雪花,我们迫不及待地想看看你会做出什么决定!
暂停愤怒退出...
好吧,如果你看不出来,那我开玩笑的。使用可观察对象意味着你可以“直接”改变可观察对象,你的下游状态就会做出相应的反应。你可能已经注意到,上面的代码示例我一直用// reaction
、// action
和来注释// observable state
,它们的意思和英语中通常的意思一样。我们稍后再讨论这个问题。
回到代码!假设我们现在有一个名为 的异步 API fetchAllCaps
。它Promise
基本上会在等待 1 秒后将你传递给它的任何文本首字母大写。所以这模拟了你想要执行的任何异步操作的基本请求-响应流程。让我们将它插入到我们目前的示例中!
class State {
text = ""; // observable state
onChange = e => {
// action
this.text = e.target.value;
fetchAllCaps(e.target.value).then(val => (this.text = val));
};
}
decorate(State, { text: observable });
const appState = new State();
嗯,那很容易吗?
请注意,这里我们对该属性使用了公共类字段第 2 阶段功能onChange
,而不使用同样属于第 2 阶段的装饰器。我决定这样做是因为公共类字段在 React 中非常普遍(例如,它附带create-react-app
),您可能已经设置了它,或者可以弄清楚如何在 Babel 中进行设置(如果需要)。
概念突破!回顾一下!
到目前为止,我们还没有讨论 MobX 的核心概念,因此它们如下:
- 可观察状态
- 行动
- 推导(反应和计算值)
在上面的例子中,我们已经使用了可观察状态以及修改这些状态的已定义操作mobx-react
,并且我们使用了@observer
来帮助绑定 React 组件以响应状态变化。所以这是 4 个中的 3 个。我们来看看计算值吧?
示例 C:计算值
计算值本质上是没有副作用的反应。由于可观察对象默认是惰性的,因此 MobX 能够根据需要延迟计算。它们只会在可观察状态更新时才更新。换句话说,计算值源自可观察状态。
让我们添加一个计算值,它只是反转中的所有内容text
:
class State {
text = "";
get reverseText() {
return this.text
.split("")
.reverse()
.join("");
}
onChange = e => {
// action
this.text = e.target.value;
fetchAllCaps(e.target.value).then(val => (this.text = val));
};
}
decorate(State, { text: observable, reverseText: computed });
const appState = new State();
// lower down...
const Display2 = inject(["store"])(
observer(({ store }) => <div>Display: {store.reverseText}</div>)
);
太棒了!它“真的有效”(TM)!
看到这里,你可能会想:为什么要这么做?我明明可以把同步业务逻辑放在 Reactrender
函数里,为什么还要在 appState 层计算呢?
在这个小例子中,这样的批评还算合理,但想象一下,如果你在应用中的多个地方依赖相同的计算值,那会是怎样的体验?你得把相同的业务逻辑复制到各个地方,或者提取到一个文件,然后再导入到各个地方。计算值是建模状态派生的好方法,因为它更靠近状态,而不是靠近视图。这虽然只是细微的差别,但在规模化应用时却可能产生重大影响。
顺便说一句,vue.js也有计算变量,而Angular只是隐式地使用它们。
示例 D1:可观察数组
MobX 基本上可以让任何东西都可观察。我来引用一下文档:
- 如果值是 ES6 Map,则将返回一个新的Observable Map。如果您不想只对特定条目的变化做出反应,还想对条目的添加或删除做出反应,那么 Observable Map 非常有用。
- 如果值是一个数组,则将返回一个新的Observable 数组。
- 如果 value 是一个没有原型的对象,则其所有当前属性都将变为可观察的。参见可观察对象
- 如果值是一个带有原型的对象、JavaScript 原语或函数,则会返回一个Boxed Observable。MobX不会自动将带有原型的对象设置为可观察对象;因为这是其构造函数的责任。请在构造函数中使用 extendsObservable,或在类定义中使用 @observable。
在上面的例子中,我们迄今为止一直在制作Boxed Observables和Observable Objects,但如果我们想要制作一个可观察数组怎么办?
可观察数组是类似数组的对象,而不是真正的数组。这可能会给用户带来麻烦,尤其是在将数据传递给其他库时。要转换为普通的 JS 数组,请调用observable.toJS()
或observable.slice()
。
但大多数时候,你可以直接将数组视为数组。这是一个使用可观察数组的非常简单的 Todo 应用:
class State {
text = ["get milk"]; // observable array
onSubmit = e => this.text.push(e); // action
}
decorate(State, { text: observable });
const appState = new State();
const Display = inject(["store"])(
observer(({ store }) => (
<ul>Todo: {store.text.map(text => <li key={text}>{text}</li>)}</ul>
))
);
const Input = observer(
["store"],
class Input extends React.Component {
render() {
// reaction
return (
<form
onSubmit={e => {
e.preventDefault();
this.props.store.onSubmit(this.input.value);
this.input.value = "";
}}
>
<input type="text" ref={x => (this.input = x)} />
</form>
);
}
}
);
const App = () => (
<React.Fragment>
<Display />
<Input />
</React.Fragment>
);
请注意,“just push
” 才有效!
示例 D2:可观察映射
可观察对象(我们在示例 A、B 和 C 中使用的对象)和可观察映射(Observable Maps)之间有什么区别?嗯,这和普通的旧式 JavaScript 对象(Plain Old JavaScript Objects)和ES6 映射(Maps)之间的区别是一样的。我将引用 MobX 文档来解释何时使用映射(Maps)而不是对象(Objects):
如果您不想仅对特定条目的变化做出反应,而且还想对条目的添加或删除做出反应,那么可观察的地图非常有用。
所以,如果我们想要一堆待办事项列表,并且可以在其中添加新的待办事项,这就是正确的抽象。因此,如果我们从示例 D1 中取出那个应用程序,将其重命名为 ,TodoList
并进行todolist.js
一些其他简单的调整,那么index.js
我们可以这样做:
// index.js
const connect = str => Comp => inject([str])(observer(Comp)); // helper function
const listOfLists = observable.map({
Todo1: new TodoListClass(),
Todo2: new TodoListClass()
// observable map rerenders when you add new members
});
const addNewList = e => listOfLists.set(e, new TodoListClass());
const App = connect("lists")(
class App extends React.Component {
render() {
const { lists } = this.props;
return (
<div className="App">
<span />
<h1>MobX Kanban</h1>
<span />
{Array.from(lists).map((k, i) => (
<div key={i}>
{/*Provider within a Provider = Providerception */}
<Provider todolist={k}>
<TodoList />
</Provider>
</div>
))}
<div>
<h3>Add New List</h3>
<form
onSubmit={e => {
e.preventDefault();
addNewList(this.input.value);
this.input.value = "";
}}
>
<input type="text" ref={x => (this.input = x)} />
</form>
</div>
</div>
);
}
}
);
瞧!我们有了看板(一个可扩展的列表列表)!
这是通过 Observable Map 的动态扩展功能实现的listOfLists
。说实话,你可能也可以使用数组来实现这一点,但如果你有更适合演示 Observable Map 的用例,请在下面的评论中告诉我。
示例 E1:MobX 开发工具
Redux 开发工具(理所当然地)是 Redux 价值的重要组成部分,所以让我们来看看MobX React 开发工具吧!
import DevTools from 'mobx-react-devtools'; // npm install --save-dev mobx-react-devtools
// somewhere within your app...
<DevTools />
您可以看到弹出三个图标:
- 可视化重新渲染
- 审计依赖关系树
- 将所有内容记录到控制台(使用浏览器控制台而不是 Codepen 控制台)
虽然您无法进行时间旅行,但这是一套非常好的工具,可以审核应用程序中发生的任何意外状态变化。
敬请关注...
mobx-dev-tools
和4有一个阻塞错误mobx
:https://github.com/mobxjs/mobx-react-devtools/issues/86,当错误修复后,我将完成此操作。
然而与此同时,你可以看看如何明确定义,actions
以便 MobX 可以批量处理你的状态变化transaction
,这可以大大节省性能:
https://mobx.js.org/refguide/action.html
请注意,我们无需使用 s 就能完成所有演示action
- MobX 有一个(记录不充分的)严格模式(以前是useStrict
,现在是configure({enforceActions: true});
) - 请参阅MobX 4 文档。但我们需要开发工具来真正展示示例应用的优势。
致谢
本介绍借鉴了Michel Weststrate 的 egghead.io 课程的大量代码和结构,但更新了这门已有两年历史的课程,使其适用于当前的 Mobx 4 API。我还要感谢我的雇主允许我公开学习。
这里的例子是在Javid Askerov、Nader Dabit和Michel的帮助下完成的。
其他教程和进一步阅读
其他近期指南
文档
较旧
相关库探索
贡献
我还应该在本指南中包含哪些现有(不到一年)的资源?我犯了什么错误吗?请在下方留言告诉我!
文章来源:https://dev.to/swyx/introduction-to-mobx-4-for-reactredux-developers-3k07