如何:mobx-state-tree + react + typescript

2025-06-09

如何:mobx-state-tree + react + typescript

本指南将指导您如何在应用程序中使用mobx-state-tree和进行完整设置。本指南不会过多关注理论或底层工作原理,主要包含实际示例(代码!)来演示如何操作。reactCRAtypescript

redux我在所有工作和业余项目中主要使用它,最终对状态管理世界的另一面产生了好奇mobx,并决定直接进入mobx-state-tree

尝试用withmobx-state-tree工作似乎相当费劲。尤其是要把所有东西都正确输入(不能作弊!)更是一大挑战。所以,当一切最终都准备就绪后,我想分享我的设置,以便(希望)让其他人的工作更轻松 :)reacttypescriptanyTypescript

我开发的应用程序是一个简单的投票生成器,可以创建新的投票、发布投票、查看和删除已发布的投票。源代码和一个简单的演示可以在我的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获取该操作getParentmobx-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 中编译。selfPollDraftChoiceModelpollDraftParent.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。让我们看看如何实现这一点,并在两个商店之间建立一些沟通。

根存储

让我们创建一个根存储来组合应用程序中使用的所有存储:PollDraftPublishedPolls

type RootStoreModel = Instance<typeof RootStore>

const RootStore = types.model("RootStore", {
  pollDraft: PollDraft,
  publishedPolls: PublishedPolls
})

商店之间沟通

在 Store 之间进行通信的一种方式是使用getRootfrommobx-state-tree获取根 Store,然后从那里获取所需的 Store,或者使用getParentfrom 遍历树。这对于紧密耦合的 Store(例如PollDraftPollDraftChoice)来说很有效,但如果用于耦合程度较低的 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

我最头疼的部分是如何在组件中连接reactmobx使用 store。这里的想法是,React 组件需要变得“响应式”,并开始跟踪来自 store 的可观察对象。

为什么不使用 mobx-react

实现此目的的最常见方法是使用mobx-react它提供observer封装了组件,使其能够响应更改并重新渲染,并将 store 注入到组件中。但是,我不建议使用这个库,因为:injectobserverinject

  • 当使用 时observer,组件将失去使用钩子的能力,因为它会被转换为类,更多信息请见文档建议在最佳实践中使用observeraround ,这意味着钩子几乎不能在任何地方使用。
  • inject函数相当复杂,并且不能很好地与 typescript 配合使用(参见github 问题),要求将所有存储标记为可选,然后使用!来指示它们确实存在。

mobx-react-lite 来帮忙

幸运的是,还有另一个库mobx-react-lite,它内置了钩子并提供observer包装器。值得一提的是,observer它不支持类,但有一个专用组件Observer可以包装jsx类组件中渲染部分。

这个库很容易让人混淆,因为它提供了很多钩子useObservable,比如 等等,根据文档,useComputed这些钩子将被弃用。相反,这里有一个推荐的方法,我们将遵循:

  • 使用react context提供程序传递存储,
  • 使用带有选择器的钩子访问存储,或者使用基于钩子的useContext自定义钩子注入必要的存储useInjectuseContext
  • 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中的实现方式。这个钩子与使用映射函数进行自定义注入的思路有些相似,但使用了钩子。因此,如果你的应用比较复杂,存储的内容很多,你可能只想获取所需的内容,而不必关心其余的内容。reduxmapStateToProps

最简单的钩子形式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经验reacttypescript

鏂囩珷鏉ユ簮锛�https://dev.to/margaretkrutikova/how-to-mobx-state-tree-react-typescript-3d5j
PREV
使用 ReasonML 中的状态机进行域建模
NEXT
软技能已过时。这里有一个更好的模型