如何:mobx-state-tree + react + typescript
本指南将指导您如何在应用程序中使用mobx-state-tree
和进行完整设置。本指南不会过多关注理论或底层工作原理,主要包含实际示例(代码!)来演示如何操作。react
CRA
typescript
redux
我在所有工作和业余项目中主要使用它,最终对状态管理世界的另一面产生了好奇mobx
,并决定直接进入mobx-state-tree
。
尝试用withmobx-state-tree
工作似乎相当费劲。尤其是要把所有东西都正确输入(不能作弊!)更是一大挑战。所以,当一切最终都准备就绪后,我想分享我的设置,以便(希望)让其他人的工作更轻松 :)react
typescript
any
Typescript
我开发的应用程序是一个简单的投票生成器,可以创建新的投票、发布投票、查看和删除已发布的投票。源代码和一个简单的演示可以在我的GitHub上找到。
如果您遇到本文涵盖的特定问题,可直接跳转到以下快速链接:
在 mobx-state-tree 中设置存储
我开始开发我的应用程序并设计域名区域的商店,mobx-state-tree
并立即面临以下“操作方法”:
- 如何创建基础模型并使用组合来扩展不同商店中的属性和功能,
- 如何创建一个包含代表另一个模型的嵌套项目列表的存储,并对其执行 CRUD 操作,
- 如何创建一个包含所有其他域存储的根存储,
- 商店之间如何沟通。
我认为这些可能是为任何领域设计商店时常见的问题,所以我将更详细地介绍它们并展示我的解决方案。
在我的民意调查制作应用程序中,将会有一个基础模型PollBase
、一个负责创建新民意调查的商店PollDraft
、一个用于发布民意调查的模型PublishedPoll
和一个用于发布民意调查的商店PublishedPolls
。
创建基础模型
在开始之前,请安装必要的依赖项:
yarn add mobx mobx-state-tree
现在让我们为域对象创建一个基础模型poll
,它将有一个民意调查问题和一个选项列表,以及一个具有字符串属性和 id 的选择基础模型:
import { types } from "mobx-state-tree"
const PollChoiceBase = types.model("PollChoiceBase", {
id: types.identifier,
value: types.optional(types.string, "")
})
const PollBase = types.model("PollBase", {
question: "",
choices: types.optional(types.array(PollChoiceBase), [])
})
使用组合来创建域存储
正在编辑且尚未发布的投票(我们称之为草稿投票)将具有与 相同的属性PollBase
,但也具有编辑这些属性的操作。类似地,选择草稿投票将具有PollChoiceBase
与更新操作相同的形状:
const PollDraftChoice = PollChoiceBase.actions(self => ({
setChoice(choice: string) {
self.value = choice
}))
const PollDraft = types
.compose(PollBase,
types.model({
choices: types.optional(types.array(PollDraftChoice), [])
})
)
.actions(self => ({
setQuestion(question: string) {
self.question = question
}
}))
已发布的投票无法再编辑,因此它将没有编辑操作,但它需要一个额外的属性id
才能找到它或创建指向它的外部链接:
const PublishedPoll = types.compose(
PollBase,
types.model({
id: types.identifier
})
)
嵌套列表中模型的 CRUD
草稿投票包含一个选项列表,您可以添加、编辑和删除这些选项。目前,我们提供了更新选项的操作 ( setChoice
),但没有删除现有选项或添加新选项的操作。
在这里,添加相当简单,但删除则有点棘手。我们希望能够choice.remove()
在react
组件的某个位置使用,但操作只能修改其所属的模型或其子级,因此选项不能简单地自行删除,只能由其父级删除,PollDraft
因为它“拥有”选项列表。这意味着PollDraftChoice
模型需要一个remove
操作来将其删除委托给,我们可以通过中的助手程序PollDraft
获取该操作。getParent
mobx-state-tree
以下是代码(我使用shortid来生成唯一的 ID):
import { destroy, getParent, Instance, cast } from "mobx-state-tree"
// Instance is a typescript helper that extracts the type of the model instance
type PollDraftChoiceModel = Instance<typeof PollDraftChoice>
type PollDraftModel = Instance<typeof PollDraft>
const PollDraftChoice = PollChoiceBase.actions(self => ({
...
remove() {
const pollDraftParent = getParent<PollDraftModel>(self, 2)
pollDraftParent.removeChoice(cast(self))
}
}))
const PollDraft = types.compose(...)
.actions(self => ({
...
addChoice(choice: string) {
self.choices.push({ id: shortid(), value: choice })
},
removeChoice(choiceToRemove: PollDraftChoiceModel) {
destroy(choiceToRemove)
}
}))
以下是内部发生的事情PollDraftChoice
:
getParent<PollDraftModel>(self, 2)
意味着获取上层 2 个级别的父级 - 一层直到到达items
属性,再一层直到到达PollDraft
其自身,并假设返回的父级是类型PollDraftModel
。pollDraftParent.removeChoice(cast(self))
使用cast
辅助函数告诉 TypeScript 其self
类型确实是PollDraftChoiceModel
。为什么有必要这么做?问题在于,这里的 类型 是在应用视图和操作之前的self
类型,这意味着此时类型 实际上并非,因此无法在 TS 中编译。self
PollDraftChoiceModel
pollDraftParent.removeChoice(self)
模型之间转换
让我们创建第二个域名商店来跟踪已发布的民意调查:
import { types, Instance, getSnapshot } from "mobx-state-tree"
type PublishedPollModel = Instance<typeof PublishedPoll>
type PollDraftModel = Instance<typeof PollDraft>
export const PublishedPolls = types
.model({
polls: types.optional(types.array(PublishedPoll), [])
})
.actions(self => ({
publishDraft(pollDraft: SnapshotIn<PollDraftModel>) {
const pollToPublish = { ...pollDraft, id: shortid() }
self.polls.push(pollToPublish)
}
}))
这里publishDraft
包含一份snapshot
民意调查草案。快照是mobx-state-tree
一个剥离了所有类型信息和操作的普通对象,可以自动转换为模型。
那么,为什么publishDraft
需要获取快照而不是仅仅获取PollDraftModel
?这是因为的实例PollDraftModel
无法转换为已发布的轮询,因为它会包含与不兼容的额外操作PublishedPollModel
,并且会导致运行时异常。因此,通过指定,SnapshotIn<PollDraftModel>
我们明确表示我们需要获取存在于的原始数据PollDraftModel
。
下一个问题是,publishDraft
必须从外部调用该操作,无论是从PollDraft
商店还是从某种方式RootStore
。让我们看看如何实现这一点,并在两个商店之间建立一些沟通。
根存储
让我们创建一个根存储来组合应用程序中使用的所有存储:PollDraft
和PublishedPolls
:
type RootStoreModel = Instance<typeof RootStore>
const RootStore = types.model("RootStore", {
pollDraft: PollDraft,
publishedPolls: PublishedPolls
})
商店之间沟通
在 Store 之间进行通信的一种方式是使用getRoot
frommobx-state-tree
获取根 Store,然后从那里获取所需的 Store,或者使用getParent
from 遍历树。这对于紧密耦合的 Store(例如PollDraft
和PollDraftChoice
)来说很有效,但如果用于耦合程度较低的 Store,则难以扩展。
启用 store 通信的一种方法是使用getEnv
在创建状态树时可以注入特定于环境数据的函数(来自mobx-state-tree 文档)。这样我们就可以将新创建的 store 注入到整个状态树中。需要注意的是,环境数据不能直接传递给子 store,而需要传递给根 store,否则会报错:
Error: [mobx-state-tree] A state tree cannot be made part of another state tree
as long as their environments are different.
让我们创建一个名为 的函数createStore
,类似于 的redux
函数configureStore
,它将创建所有单独的存储,创建环境,并将它们组合到一个根存储中。环境只有一个 store 属性,因为发布投票草稿时PublishedPolls
需要访问它:PollDraft
type RootStoreEnv = {
publishedPolls: PublishedPollsModel
}
const createStore = (): RootStoreModel => {
const publishedPolls = PublishedPolls.create()
const pollDraft = PollDraft.create()
const env: RootStoreEnv = { publishedPolls }
return RootStore.create({ pollDraft, publishedPolls }, env)
}
现在,PolLDraft
商店可以定义一个publish
动作并publishDraft
调用publishedPolls
:
import { types, getEnv, getSnapshot } from "mobx-state-tree"
const PollDraft = types
.compose(...)
.actions(self => ({
...
publish() {
const snapshot = getSnapshot(self)
const env = getEnv<RootStoreEnv>(self)
env.publishedPolls.publishDraft(snapshot)
}
}))
连接到 redux devtools
我们将使用connectReduxDevtools
包中的中间件mst-middlewares
将状态树连接到 redux devtools(更多信息和配置选项请参阅文档)。为了建立连接,我们将使用一个监控工具remotedev
。首先安装以下软件包:
yarn add --dev remotedev mst-middlewares
并在商店创建后添加以下代码:
import { createStore } from "../stores/createStore"
import { connectReduxDevtools } from "mst-middlewares"
const rootStore = createStore()
connectReduxDevtools(require("remotedev"), rootStore)
将 React 连接到 mobx
我最头疼的部分是如何在组件中连接react
并mobx
使用 store。这里的想法是,React 组件需要变得“响应式”,并开始跟踪来自 store 的可观察对象。
为什么不使用 mobx-react
实现此目的的最常见方法是使用mobx-react,它提供observer
并封装了组件,使其能够响应更改并重新渲染,并将 store 注入到组件中。但是,我不建议使用这个库,因为:inject
observer
inject
- 当使用 时
observer
,组件将失去使用钩子的能力,因为它会被转换为类,更多信息请见。文档建议在最佳实践中使用observer
around ,这意味着钩子几乎不能在任何地方使用。 inject
函数相当复杂,并且不能很好地与 typescript 配合使用(参见github 问题),要求将所有存储标记为可选,然后使用!
来指示它们确实存在。
mobx-react-lite 来帮忙
幸运的是,还有另一个库mobx-react-lite
,它内置了钩子并提供observer
包装器。值得一提的是,observer
它不支持类,但有一个专用组件Observer
可以包装jsx
类组件中渲染部分。
这个库很容易让人混淆,因为它提供了很多钩子useObservable
,比如 等等,根据文档,useComputed
这些钩子将被弃用。相反,这里有一个推荐的方法,我们将遵循:
- 使用
react context
提供程序传递存储, - 使用带有选择器的钩子访问存储,或者使用基于钩子的
useContext
自定义钩子注入必要的存储,useInject
useContext
observer
用from包装组件mobx-react-lite
来订阅更改。
因此让我们安装该库:
yarn add mobx-react-lite
上下文提供者传递存储
首先,让我们创建上下文StoreContext
,它稍后将接收根存储作为其value
,并导出提供程序和用于访问上下文值的自定义钩子:
const StoreContext = createContext<RootStoreModel>({} as RootStoreModel)
export const useStore = () => useContext(StoreContext)
export const StoreProvider = StoreContext.Provider
然后创建根存储createStore
并将其发送到StoreProvider
我们包装的其中App
:
import { StoreProvider } from "./StoreProvider"
import { createStore } from "../stores/createStore"
const rootStore = createStore()
const Root: React.FunctionComponent<{}> = () => (
<StoreProvider value={rootStore}>
<App />
</StoreProvider>
)
自定义钩子来注入商店
可以useStore
直接使用钩子来访问根存储并从中获取必要的数据,如下所示:
const { pollDraft } = useStore()
我还实现了一个钩子,它接受一个映射函数并返回一个映射对象,类似于withuseInject
中的实现方式。这个钩子与使用映射函数进行自定义注入的思路有些相似,但使用了钩子。因此,如果你的应用比较复杂,存储的内容很多,你可能只想获取所需的内容,而不必关心其余的内容。redux
mapStateToProps
最简单的钩子形式useInject
可能看起来像这样:
export type MapStore<T> = (store: RootStoreModel) => T
const useInject = <T>(mapStore: MapStore<T>) => {
const store = useStore()
return mapStore(store)
}
然后组件PollDraft
将使用从根存储useInject
访问存储:pollDraft
import { observer } from "mobx-react-lite"
import { RootStoreModel } from "../stores/RootStore"
import useInject from "../hooks/useInject"
const mapStore = (rootStore: RootStoreModel) => ({ pollDraft: rootStore.pollDraft })
const PollDraft: React.FunctionComponent<{}> = observer(() => {
const { pollDraft } = useInject(mapStore)
return (
<div>
<h1>Create a new poll</h1>
<input
value={pollDraft.question}
onChange={e => pollDraft.setQuestion(e.target.value)}
/>
<button onClick={pollDraft.publish}>Publish</button>
</div>
)
})
mapStore
如果功能更复杂并且涉及组合来自多个商店的数据和操作,这将特别有用。
至此,我感觉我已经掌握了基础知识,并创建了一个可以继续构建或用作类似技术栈项目的样板的设置。源代码可以在我的GitHub上找到。
希望本教程对您有所帮助,并希望您能从中找到一些对您项目有帮助的内容。我们非常乐意听取您的反馈,或者分享您使用 和的mobx-state-tree
经验!react
typescript