Вы когда-нибудь использовали mobx-state-tree?

Для тех, кто этого не знает, это классная библиотека управления состоянием. Он предлагает простоту использования изменяемой древовидной структуры, которая автоматически генерирует структурно разделяемые неизменяемые снимки, исправления, проверку типа во время выполнения, сериализацию и воспроизведение действий, и так далее. Все эти удовольствия - то, что превратило меня в одного из их сопровождающих и яростного сторонника этого (и я остаюсь им до сих пор!).

Однако нет ничего идеального, и после того, как я использовал его для двух больших проектов, я видел, как разработчики (включая меня) временами борются с этим, это то, что я бы назвал, его болевые точки .

Поскольку эти болевые точки являются основной причиной создания mobx-keystone, я хотел бы показать их, а затем представить альтернативное решение.

Боль # 1: поддержка машинописного текста

Хотя его поддержка Typescript намного лучше, чем раньше, все еще есть области, которые не были рассмотрены (и, вероятно, с текущими возможностями Typescript или без редизайна API не может быть вообще) .

Возьмем, например, саморекурсивную модель, такую ​​как дерево или модели с перекрестными ссылками (модели, которые ссылаются друг на друга). Хотя сама библиотека поддерживает такие структуры, очень сложно заставить Typescript поддерживать их, не прибегая к странным уловкам или просто прибегая к any. Не говоря уже о том, что типизация настолько сложна, что новые версии Typescript рискуют сломать ее (хотя исправления приходят быстро).

mobx-keystone был создан с менталитетом «Сначала машинописный текст», до такой степени, что вам даже не нужно использовать что-либо еще для объявления типов, если вам не требуется проверка типов во время выполнения. Например, правильно набрать дерево, состоящее из саморекурсивных узлов, просто:

// self recursive model
@model(“myApp/TreeNode”)
class TreeNode extends Model({
  children: prop<TreeNode[]>(() => [])
}) {}

И делать перекрестные ссылки на модели так же просто:

// cross-referenced models
@model("myApp/A")
class A extends Model({
  b: prop<B | undefined>()
}) {}
@model("myApp/B")
class B extends Model({
  a: prop<A | undefined>()
}) {}

Другими словами, mobx-keystone, когда не использует проверку типов во время выполнения, использует стандартные аннотации типов Typescript для объявления данных моделей, тем самым снижая кривую обучения. Однако, если вам нужна проверка типов во время выполнения, mobx-keystone также включает полностью необязательную систему определения типов / проверки типов во время выполнения.

Проблема №2: Экземпляры, входные снимки, выходные снимки, приведение…

В mobx-state-tree можно назначать моментальные снимки свойствам, а также фактическим экземплярам, ​​но фактический тип свойств - это экземпляры, что приводит к запутанным приведениям и конструкциям, таким как:

// mobx-state-tree code
const Todo = types.model({
  done: false,
  text: types.string
})
.actions(self => ({
  setText(text: string) {
    self.text = text
  },
  setDone(done: boolean) {
    self.done = done
  }
}))
const RootStore = types.model({
  selected: types.maybe(Todo))
})
.actions(self => ({
  // note the usage of a union of the snapshot type
  // and the instance type
  setSelected(todo: SnapshotIn<typeof Todo> | Instance<typeof Todo>) {
    // note the usage of cast to indicate that it is ok to use
    // a snapshot when the property actually expects an instance
    self.selected = cast(todo)
  }
}))

Обратите внимание, как действие setSelected может фактически принимать входной снимок (простой объект Javascript) или экземпляр (экземпляр объекта mobx-state-tree) в качестве входных данных, а также преобразование, чтобы Typescript хорошо ладил с простыми объектами Javascript, которые автоматически преобразуются в экземпляры при присвоение. Тогда просто представьте, что вам нужно объяснить это другому разработчику, плохо знакомому с этой технологией.

В mobx-keystone создание снимков обычно ожидается только при работе с getSnapshot и fromSnapshot, то есть только при фактическом использовании сценариев сериализации. Это приводит к меньшей путанице и более явному использованию:

// mobx-keystone code
@model("myApp/Todo")
class Todo extends Model({
  done: prop(false),
  text: prop<string>(),
}) {
  @modelAction
  setText(text: string) {
    this.text = text
  }
  @modelAction
  setDone(done: boolean) {
    this.done = done
  }
}
@model("myApp/RootStore")
class RootStore extends Model({
  selected: prop<Todo | undefined>(undefined),
}) {
  @modelAction
  setSelected(todo: Todo | undefined) {
    this.selected = todo
  }
}

Боль №3: это, себя, фрагменты действий, фрагменты просмотров…

При использовании mobx-state-tree с Typescript, чтобы получить правильную типизацию, к коду из предыдущего «фрагмента» (действия, представления и т. Д.) Необходимо обращаться с помощью self, в то время как код в том же «фрагменте» должен быть доступ осуществляется с помощью this.

// mobx-state-tree code
const Todo = types
  .model({
    done: false,
    text: types.string,
    title: types.string,
  })
  .views(self => ({
    get asStr() {
      // here we use self since the properties
      // come from a previous chunk
      return `${self.text} is done? ${self.done}`
    },
    get asStrWithTitle() {
      // here we use this for asStr since it
      // comes from the current chunk
      return `${self.title} - ${this.asStr}`
    },
  }))

В mobx-keystone this - единственный способ получить доступ к текущему экземпляру, нет необходимости искусственно отделять фрагменты действий от фрагментов представления, плюс можно использовать стандартный декоратор mobx computed, что значительно упрощает переход от простых «классов» mobx. понять.

// mobx-keystone code
@model("myApp/Todo")
class Todo extends Model({
  done: prop(false),
  text: prop<string>(),
  title: prop<string>(),
}) {
  @computed
  get asStr() {
    return `${this.text} is done? ${this.done}`
  }
  @computed
  get asStrWithTitle() {
    return `${this.title} - ${this.asStr}`
  }
}

Боль no 4: жизненный цикл модели

mobx-state-tree имеет несколько хуков жизненного цикла (afterCreate, afterAttach, beforeDetach, beforeCreate), которые могут или не могут срабатывать, когда вы думаете, что они должны, из-за ленивой инициализации узлов.

Например, вы можете создать подмодель с помощью хука afterCreate, но она может никогда не выполняться на самом деле, если не выполняется доступ к содержимому узла (из-за ленивой инициализации). Возможно, вы захотите настроить эффект (reaction и т.п.), но вы хотите, чтобы этот эффект работал только после того, как он фактически станет частью состояния вашего приложения. Точно так же вы можете получить доступ к getRoot для доступа к корневой модели, но они могут фактически не дать ожидаемого значения, пока они не будут прикреплены к родительскому элементу, который в конечном итоге (или нет) присоединен к правильному корню.

mobx-keystone решает эту проблему, предлагая только два крючка жизненного цикла, onInit, который всегда вызывается после создания модели (и поскольку нет ленивой инициализации, они всегда будут), и onAttachedToRootStore ( плюс необязательный распределитель, который запускается при отсоединении), который вызывается, когда модель присоединяется к правильному корневому узлу (корневому хранилищу), тем самым гарантируя, что в этот момент getRoot вернет ожидаемое значение и сделает его идеальным местом для настройки эффектов.

Боль №5: И еще немного.

Ссылки в mobx-state-tree были разработаны так, чтобы быть прозрачными для пользователя. Может быть, слишком прозрачно, до уровня, когда, например, невозможно получить родительский элемент ссылочного узла. В mobx-keystone ссылки являются явными объектами, что делает, например, этот вариант использования тривиальным.

Промежуточные программы действий не были созданы с учетом асинхронности (потоков на жаргоне mobx), что делает их довольно трудными для использования в таких случаях. mobx-keystone в своем промежуточном программном обеспечении гарантирует, что (асинхронные) потоки так же просты в использовании, как и действия синхронизации.

Резюме

Эта статья никоим образом не намеревалась уничтожить mobx-state-tree (опять же, это круто!), А просто чтобы выявить некоторые из его болевых точек. Может быть, он вам очень нравится, и он идеально подходит для вашего приложения. Хорошо!

Но если вы тоже почувствовали некоторые из этих болевых точек, я предлагаю вам перейти на https://mobx-keystone.js.org и попробовать!