三、使用 MobX 的 React 应用
和 React 一起工作很有趣。现在,将其与 MobX 结合起来,满足您所有的状态管理需求,您就拥有了一个超级组合。有了 MobX 的基础知识,我们现在可以冒险使用到目前为止讨论的想法构建一个简单的 React 应用。我们将处理定义可观察状态的过程、可在该状态上调用的操作以及将观察和呈现变化状态的 React UI。
本章涵盖的主题包括以下内容:
- 图书搜索用例
- 创建可观察的状态和动作
- 构建反应式用户界面
技术要求
最后,要使用本书的 Git 存储库,用户需要安装 Git。
本章代码文件可在 GitHub 上找到: https://github.com/PacktPublishing/MobX-Quick-Start-Guide/tree/master/src/Chapter03
查看以下视频以查看代码的运行: http://bit.ly/2v0HnkW
图书搜索
我们的 simple React 应用的用例来自传统的电子商务应用,即在巨大的库存中搜索产品。在我们的例子中,搜索是为了寻找书籍。我们将使用GoodreadsAPI 按书名或作者搜索一本书。Goodreads 要求我们注册一个帐户来使用他们的 API
Create a Goodreads account by visiting this URL: https://www.goodreads.com/api/keys. You can use your Amazon or Facebook account to log in. Once you have the account, you need to generate an API key to make the API calls.
Goodreads 公开了一组以 XML 形式返回结果的端点。同意,这并不理想,但他们有大量的书籍,而且将 XML 转换为 JSON 对象的代价很小。事实上,我们将使用npm
包进行此转换。我们将使用的端点是搜索书籍(https://www.goodreads.com/search/index.xml?key=API_KEY &q=搜索项
我们的应用的 UI 将如下所示:
即使在这个看起来相当简单的界面中,也有一些非平凡的用例。由于我们正在进行网络调用以获取结果,因此在显示结果的列表之前,我们有一个中间状态等待结果。此外,现实世界是严酷的,您的网络呼叫可能失败或返回零结果。所有这些状态都将在我们的 React UI 中通过 MobX 进行处理
可观察的状态和行为
UI 只是一个宏大的数据转换。它也是此数据的观察者,并启动操作来更改它。由于数据(又名状态)对于 UI 来说非常重要,因此我们首先对该状态进行建模是有意义的。使用 MobX 时,可观察对象表示该状态。回顾之前的 UI 设计,我们可以确定可观察状态的各个部分:
- 这是用户键入的搜索文本。这是一个字符串类型的
observable
字段。 - 有一系列可观察的结果。
- 有关于结果的元信息,例如当前子集和总结果计数。
- 有一些状态可以捕获我们将调用的
async search()
操作。操作的初始status
为empty
。一旦用户调用搜索,我们就处于pending
状态。搜索完成后,我们可能处于completed
或failed
状态。这看起来更像是<empty>
、pending
、completed
或failed
的枚举,可以通过observable
字段捕获。
由于所有这些状态属性都是相关的,我们可以将它们放在一个可观察对象下:
const searchState = observable({
term: '',
state: '',
results: [],
totalCount: 0,
});
这当然是一个很好的开始,似乎抓住了我们需要在 UI 上显示的大部分内容。除了状态之外,我们还需要确定可以在 UI 上执行的操作。对于我们的简单 UI,这包括调用搜索和在用户在文本框中键入字符时更新术语。MobX 中的操作被建模为动作,动作在内部改变可观察状态。我们可以将这些作为searchState
可观察到的动作添加:
const searchState = observable({
term: '',
status: '',
results: [],
totalCount: 0,
search: action(function() {
// invoke search API
}),
setTerm: action(function(value) {
this.term = value;
}),
});
searchState
可观测值的大小正在缓慢增长,并且在定义可观测状态时积累了一些语法噪音。随着我们添加更多的可观察字段、计算属性和操作,这肯定会变得更加笨拙。更好的建模方法是使用类和装饰器。
There is a little caveat with the way we have defined the actions for the searchState
observable. Note that we have deliberately avoided the use of arrow-functions to define the action. This is because arrow-functions capture the lexical this at the time the action is defined. However, the observable()
API returns a new object, which is of course different from the lexical this that is captured in the action()
call. This means, the this
that you are mutating would not be the object that is returned from observable()
. You can try this out by passing arrow-functions into the action()
calls.
By passing a plain-function into the action()
, we can be assured that this
would point to the correct instance of the observable.
让我们看看类和装饰器的外观:
class BookSearchStore {
@observable term = '';
@observable status = '';
@observable.shallow results = [];
@observable totalCount = 0;
@action.bound
setTerm(value) {
this.term = value;
}
@action.bound
async search() {
// invoke search API
}
}
export const store = new BookSearchStore();
使用 decorator 可以很容易地看到类的可观察字段。事实上,我们可以灵活地将可观测场与规则场混合匹配。装饰师还可以轻松调整可观察性级别(例如:结果的shallow
可观察性)。BookSearchStore
类在装饰器的帮助下捕获可观察的字段和动作。因为我们只需要这个类的一个实例,所以我们将单例实例导出为store
。
管理异步操作
通过async search()
动作,事情变得更加有趣。我们的 UI 需要知道操作在任何时间点的确切状态。为此,我们有一个可观察的字段:status
,它跟踪操作状态。最初从empty
状态开始,在操作开始时进入pending
。一旦操作完成,它可以处于completed
或failed
状态。您可以在代码中看到这一点,如下所示:
class BookSearchStore {
@observable term = '';
@observable status = '';
@observable.shallow results = [];
@observable totalCount = 0;
/* ... */
@action.bound
async search() {
try {
this.status = 'pending';
const result = await searchBooks(this.term);
runInAction(() => {
this.totalCount = result.total;
this.results = result.items;
this.status = 'completed';
});
} catch (e) {
runInAction(() => (this.status = 'failed'));
console.log(e);
}
}
}
在前面的代码中,有几点很突出:
async
动作与sync
动作差别不大。事实上,异步动作只是在不同时间点上的同步动作- 设置可观察状态只是分配问题。我们将代码包装在
await
之后的runInAction()
中,以确保在动作中所有可观察到的都发生了变化。当我们打开 MobX 的enforceActions
配置时,这成为关键。 - 因为我们使用的是
async-await
,所以我们在一个地方处理两种未来的可能性。 searchBooks()
函数只是一个调用 Goodreads API 并获取结果的服务方法。它返回一个承诺,我们在async
动作中await
承诺。
此时,我们已经准备好了应用的可观察状态,以及可以对这些可观察状态执行的一组操作。我们将创建的 UI 只是绘制这个可观察的状态,并公开控件以调用操作。让我们直接进入 UI 的观察者领域。
One observation you can make in the async search()
method just seen is the wrapping of the state mutation in runInAction()
. This can get tedious if you have multiple await
calls with state mutation in between those calls. Diligently wrapping each of those state-mutations can be cumbersome and you may even forget to wrap!
To avoid this unwieldy ceremony, you could use a utility function called flow()
, which takes in a generator
function and, instead of await
, uses the yield
operator. The flow()
utility correctly wraps the state-mutations following a yield
within action()
, so you don't have to do it yourself. We will use this approach in a later chapter.
反应式用户界面
在 MobX 的核心三元组中,反应扮演着影响外部世界的角色。在第 2 章观察物、动作和反应中,我们已经看到了一些以autorun()
、reaction()
和when()
形式出现的反应:
observer()
是另一种有助于将 React 世界绑定到 MobX 的反应。observer()
是mobx-react
NPM 包的一部分,是 MobX 和 React 的绑定库。它创建了一个高阶组件(HOC),该组件封装了一个 React 组件,以便在可观察状态发生变化时自动更新。在内部,observer()
跟踪在组件的render
方法中取消引用的观测值。当其中任何一个发生更改时,将触发组件的重新渲染
It is quite common to sprinkle observer()
components throughout the UI component tree. Wherever an observable is required to render the component, an observer()
can be used.
我们想要构建的 UI 将把BookSearchStore
的可观察状态映射到各个组件。让我们将 UI 分解为其结构组件,如下图所示。此处的观察者组件包括SearchTextField和ResultsList:
When you start out mapping the observable state to React components, you should start with one monolithic component that reads all the necessary state and renders it out. Then, you can start splitting the observer-components and gradually create the component hierarchy. It is recommended to get as granular as you can with your observer-components. This ensures React is not unnecessarily rendering the entire component when only a small part of it is changing.
在最高级别,我们有组成SearchTextField
和ResultsList
的App
组件。在代码中,如下所示:
import {inject, observer} from 'mobx-react'; @inject('store')
@observer
class App extends React.Component {
render() {
const { store } = this.props;
return (
<Fragment>
<Header />
<Grid container>
<Grid item xs={12}>
<Paper elevation={2} style={{ padding: '1rem' }}>
<SearchTextField
onChange={this.updateSearchText}
onEnter={store.search}
/>
</Paper>
</Grid>
<ResultsList style={{ marginTop: '2rem' }} />
</Grid>
</Fragment>
);
}
updateSearchText = event => {
this.props.store.setTerm(event.target.value);
};
}
如果你已经注意到了,那么App
类中有一个我们以前从未见过的新装饰师:inject('store')
,也是mobx-react
包的一部分。这将创建一个 HOC,将可观察到的store
绑定到 React 组件。这意味着,在App
组件的render()
中,我们可以期望props
上有store
属性。
我们正在为各种 UI 组件使用material-ui
NPM 包。该组件库为我们的 UI 提供了一种材料设计外观,并提供了许多实用组件,如TextField
、LinearProgress
、Grid
等。
去商店
使用inject()
,您可以将可观察的BookSearchStore
连接到任何反应组件。然而,神秘的问题是:如何inject()
知道我们的BookSearchStore
这就是你需要看到App
组件之上的一个级别发生了什么,我们在这里呈现整个 React 应用:
import { store } from './BookStore';
import React, { Fragment } from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'mobx-react';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'),
);
来自mobx-react
的Provider
组件建立了与BookSearchStore
可见的真实连接胶。导出的BookSearchStore
(名为store
)单例实例作为名为store
的道具传递到Provider
。在内部,它使用 React 上下文将store
传播到由inject()
装饰器包装的任何组件。因此,Provider
提供store
可观察且inject()
连接反应上下文(由Provider
公开),并将store
注入到被包装组件中:
值得注意的是,命名道具store
没有什么特别之处。您可以选择任何您喜欢的名称,甚至可以将多个可观察实例传递到Provider
。如果我们的简单应用需要为用户偏好建立一个单独的商店,我们可以按如下方式传递它:
import { store } from './BookStore';
import { preferences } from 'PreferencesStore;
<Provider store={store} userPreferences={preferences}>
<App />
</Provider>
当然,这意味着inject()
也会将其引用为userPreferences
:
@inject('userPreferences')
@observer
class PreferencesViewer extends React.Component {
render() {
const { userPreferences } = this.props;
/* ... */
}
}
SearchTextField 组件
回到我们最初的示例,我们可以利用Provider
和inject()
的功能在组件树的任何级别访问store
(一个BookSearchStore
的实例)。SearchTextField
组件利用其成为store
的观察员:
@inject('store')
@observer
export class SearchTextField extends React.Component {
render() {
const { store, onChange } = this.props;
const { term } = store;
return (
<Fragment>
<TextField
placeholder={'Search Books...'}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Search />
</InputAdornment>
),
}}
fullWidth={true}
value={term}
onChange={onChange}
onKeyUp={this.onKeyUp}
/>
<SearchStatus />
</Fragment>
);
}
onKeyUp = event => {
if (event.keyCode !== 13) {
return;
}
this.props.onEnter();
};
}
SearchTextField
观察store
的term
属性,并在其发生变化时更新自身。对term
的更改作为TextField
的onChange
处理程序的一部分进行处理。实际的onChange
处理程序由App
组件作为道具传递到SearchTextField
。在App
组件中,我们触发setTerm()
操作以更新store.term
属性:
@inject('store')
@observer
class App extends React.Component {
render() {
const { store } = this.props;
return (
<Fragment>
<Header />
<Grid container>
<Grid item xs={12}>
<Paper elevation={2} style={{ padding: '1rem' }}>
<SearchTextField
onChange={this.updateSearchText} onEnter={store.search}
/>
</Paper>
</Grid>
<ResultsList style={{ marginTop: '2rem' }} />
</Grid>
</Fragment>
);
}
updateSearchText = event => {
this.props.store.setTerm(event.target.value);
};
}
现在,SearchTextField
不仅处理对store.term
可观察对象的更新,而且还使用SearchStatus
组件显示搜索操作的状态。我们在SearchTextField
中包含此组件,但没有传递任何道具。起初,这似乎有点令人不安。SearchStatus
究竟如何了解当前的store.status
?那么,一旦你看一下SearchStatus
的定义,这应该是显而易见的:
import React, { Fragment } from 'react';
import { inject, observer } from 'mobx-react';
export const SearchStatus = inject('store')(
observer(({ store }) => {
const { status, term } = store;
return (
<Fragment>
{status === 'pending' ? (
<LinearProgress variant={'query'} />
) : null}
{status === 'failed' ? (
<Typography
variant={'subheading'}
style={{ color: 'red', marginTop: '1rem' }}
>
{`Failed to fetch results for "${term}"`}
</Typography>
) : null}
</Fragment>
);
}),
);
使用inject()
,我们可以访问store
可观察状态,通过使用observer()
包装组件,我们可以对可观察状态的变化做出反应(term
、status
。注意对inject('store')(observer( () => {} ))
的嵌套调用的使用。这里的顺序很重要。您首先通过请求要注入的提供者道具来调用inject()
。这将返回一个将组件作为输入的函数。这里我们使用observer()
创建一个 HOC 并将其传递给inject()
。
由于SearchStatus
组件基本上是独立的,SearchTextField
可以简单地包含它,并期望它能够正常工作
当store.status
更改时,只有SearchStatus
的虚拟 DOM 更改,只重新呈现该组件。SearchTextField
的其余部分保持不变。这种渲染效率内置于observer()
中,您无需额外工作。在内部,observer()
仔细跟踪render()
中使用的观察值,并设置reaction()
以在任何跟踪观察值发生变化时更新组件。
ResultsList 组件
使用SearchTextField
,当您键入一些文本并点击输入时,将调用搜索操作。这会改变可观察状态,部分由SearchTextField
呈现。但是,当结果到达时,匹配搜索词的图书列表将由ResultsList
组件显示。正如所料,它是一个观察者组件,通过inject()
连接到store
观察者。但这一次,它使用了一种稍微不同的方式连接到store
:
import { inject, observer } from 'mobx-react';
@inject(({ store }) => ({ searchStore: store }))
@observer
export class ResultsList extends React.Component {
render() {
const { searchStore, style } = this.props;
const { isEmpty, results, totalCount, status } = searchStore;
return (
<Grid spacing={16} container style={style}>
{isEmpty && status === 'completed' ? (
<Grid item xs={12}>
<EmptyResults />
</Grid>
) : null}
{!isEmpty && status === 'completed' ? (
<Grid item xs={12}>
<Typography>
Showing <strong>{results.length}</strong>
of{' '}
{totalCount} results.
</Typography>
<Divider />
</Grid>
) : null}
{results.map(x => (
<Grid item xs={12} key={x.id}>
<BookItem book={x} />
<Divider />
</Grid>
))}
</Grid>
);
}
}
注意@inject
装饰器的使用,它接受一个函数来提取store
可观测值。这为您提供了一种更为类型安全的方法,而不是使用字符串属性。您还会看到,在提取器函数中,我们将store
重命名为searchStore
。因此,可观察的store
被注入名称searchStore
。
在ResultsList
的渲染方法中,我们正在做一些其他值得一提的事情:
- 使用
isEmpty
属性检查搜索结果是否为空。这在前面没有声明,但实际上是一个computed
属性,用于检查结果数组的长度,如果为零,则返回true
:
class BookSearchStore {
@observable term = 'javascript';
@observable status = '';
@observable.shallow results = [];
@observable totalCount = 0;
@computed
get isEmpty() {
return this.results.length === 0;
}
/* ... */
}
如果搜索操作已完成且未返回任何结果(isEmpty = true
,则显示EmptyResults
组件。
- 如果搜索完成,我们得到了一些结果,我们将显示计数和结果列表,每个结果都使用
BookItem
组件呈现。
因此,我们应用的组件树如下所示:
提供者实际上是可观察状态的提供者。它依赖于 React 上下文传播组件子树中可观察到的store
。通过使用inject()
和observer()
装饰组件,您可以连接到可观察状态并对变化做出反应。SearchTextField、SearchStatus和ResultsList组件依赖observer()
和inject()
为您提供反应式 UI。
**With the introduction of React.createContext()
in React 16.3+, you can create your own Provider
component if you wish. It might be a little verbose, but it achieves the same purpose—propagating the store across the component sub-tree. Give it a shot, if you feel a little adventurous.
总结
mobx
和mobx-react
是两个广泛用于构建反应式 UI 的 NPM 包。mobx
包提供了构建可观察状态、动作和反应的 API。另一方面,mobx-react
提供粘合胶,将反应组分连接到可观察状态,并对任何变化作出反应。在我们的示例中,我们使用这些 API 构建了一个图书搜索应用。创建您的观察者驱动的组件树时,请确保使用观察者进行细化。这样,您将只对呈现 UI 所需的可观察对象做出反应。
创建SearchTextField
、SearchStatus
和ResultsList
组件的目的是使其呈颗粒状,并与聚焦的可观察表面发生反应。这是将 MobX 与 React 一起使用的推荐方法。
在下一章中,我们将深入研究 MobX,并对可观测的物体进行探索。***
版权属于:月萌API www.moonapi.com,转载请注明出处