データストアの定義
このセクションでは、MendixでMobXを使用する際に発見した、大規模で保守可能なプロジェクトを構築するためのベストプラクティスの一部を紹介します。このセクションは意見に基づいており、これらのプラクティスを適用する義務はありません。MobXとReactの使用方法には多くの方法があり、これはそのうちの1つに過ぎません。
このセクションでは、既存のコードベースや従来のMVCパターンでも効果的な、非侵襲的なMobXの使用方法に焦点を当てています。ストアを整理する他の、より意見の強い方法は、mobx-state-treeとmobx-keystoneです。どちらも、構造的に共有されたスナップショット、アクションミドルウェア、JSONパッチサポートなどをすぐに利用できます。
ストア
ストアは、あらゆるFluxアーキテクチャで見られ、MVCパターンのコントローラーと多少比較できます。ストアの主な役割は、ロジックと状態をコンポーネントからスタンドアロンでテスト可能なユニットに移し、フロントエンドとバックエンドのJavaScriptの両方で使用できるようにすることです。
ほとんどのアプリケーションは、少なくとも2つのストアを持つことでメリットがあります。1つはドメイン状態用、もう1つはUI状態用です。これら2つを分離する利点は、ドメイン状態を普遍的に再利用してテストできることであり、他のアプリケーションでも再利用できる可能性があることです。
ドメインストア
アプリケーションには、1つまたは複数のドメインストアが含まれます。これらのストアは、アプリケーションの中心となるデータを格納します。ToDoアイテム、ユーザー、書籍、映画、注文などです。アプリケーションには、少なくとも1つのドメインストアがある可能性が最も高いです。
単一のドメインストアは、アプリケーション内の単一の概念を担当する必要があります。単一のストアは、多くの場合、内部に複数のドメインオブジェクトを持つツリー構造として編成されます。
たとえば、製品用のドメインストアを1つ、注文と注文明細書用にもう1つ用意します。経験則として、2つのアイテム間の関係の性質が包含であれば、通常は同じストアにあるべきです。したがって、ストアはドメインオブジェクトを管理するだけです。
ストアの役割は以下のとおりです。
- ドメインオブジェクトのインスタンス化。ドメインオブジェクトが属するストアを認識させます。
- 各ドメインオブジェクトのインスタンスが1つだけ存在することを確認します。同じユーザー、注文、またはToDoをメモリに2回格納しないでください。これにより、参照を安全に使用し、参照を解決することなく常に最新のインスタンスを参照していることを確認できます。これは、デバッグ時に高速、シンプル、そして便利です。
- バックエンド統合を提供します。必要に応じてデータを格納します。
- バックエンドから更新を受信した場合、既存のインスタンスを更新します。
- アプリケーションのスタンドアロン、ユニバーサル、テスト可能なコンポーネントを提供します。
- ストアをテスト可能にし、サーバーサイドで実行できるようにするために、実際のWebsocket/HTTPリクエストの実行は別のオブジェクトに移すことがおそらく必要になります。これにより、通信レイヤーを抽象化できます。
- ストアのインスタンスは1つだけ存在する必要があります。
ドメインオブジェクト
各ドメインオブジェクトは、独自のクラス(またはコンストラクター関数)を使用して表現する必要があります。クライアント側のアプリケーションの状態をデータベースのようなものとして扱う必要はありません。実際の参照、循環データ構造、インスタンスメソッドは、JavaScriptの強力な概念です。ドメインオブジェクトは、他のストアのドメインオブジェクトを直接参照できます。覚えておいてください。アクションとビューをできるだけシンプルに保ちたいので、参照を管理したり、自分でガベージコレクションを実行したりする必要があるのは、後退するステップになる可能性があります。Reduxなどの多くのFluxアーキテクチャとは異なり、MobXではデータを正規化する必要がなく、アプリケーションの本質的に複雑な部分(ビジネスルール、アクション、ユーザーインターフェース)を構築することがはるかに簡単になります。
ドメインオブジェクトは、アプリケーションに適している場合、すべてのロジックを属するストアに委任できます。ドメインオブジェクトをプレーンオブジェクトとして表現することは可能ですが、クラスにはプレーンオブジェクトよりも重要な利点があります。
- メソッドを持つことができます。これにより、ドメインの概念をスタンドアロンで使いやすくなり、アプリケーションに必要なコンテキスト認識の量を減らすことができます。オブジェクトを渡すだけです。ストアを渡す必要はなく、インスタンスメソッドとして利用可能であれば、どのアクションをオブジェクトに適用できるかを判断する必要もありません。これは、大規模なアプリケーションで特に重要です。
- 属性とメソッドの可視性を細かく制御できます。
- コンストラクター関数を使用して作成されたオブジェクトは、オブザーバブルプロパティとメソッド、およびオブザーバブルではないプロパティとメソッドを自由に組み合わせることができます。
- 簡単に認識でき、厳密に型チェックできます。
ドメインストアの例
import { makeAutoObservable, runInAction, reaction } from "mobx"
import uuid from "node-uuid"
export class TodoStore {
authorStore
transportLayer
todos = []
isLoading = true
constructor(transportLayer, authorStore) {
makeAutoObservable(this)
this.authorStore = authorStore // Store that can resolve authors.
this.transportLayer = transportLayer // Thing that can make server requests.
this.transportLayer.onReceiveTodoUpdate(updatedTodo =>
this.updateTodoFromServer(updatedTodo)
)
this.loadTodos()
}
// Fetches all Todos from the server.
loadTodos() {
this.isLoading = true
this.transportLayer.fetchTodos().then(fetchedTodos => {
runInAction(() => {
fetchedTodos.forEach(json => this.updateTodoFromServer(json))
this.isLoading = false
})
})
}
// Update a Todo with information from the server. Guarantees a Todo only
// exists once. Might either construct a new Todo, update an existing one,
// or remove a Todo if it has been deleted on the server.
updateTodoFromServer(json) {
let todo = this.todos.find(todo => todo.id === json.id)
if (!todo) {
todo = new Todo(this, json.id)
this.todos.push(todo)
}
if (json.isDeleted) {
this.removeTodo(todo)
} else {
todo.updateFromJson(json)
}
}
// Creates a fresh Todo on the client and the server.
createTodo() {
const todo = new Todo(this)
this.todos.push(todo)
return todo
}
// A Todo was somehow deleted, clean it from the client memory.
removeTodo(todo) {
this.todos.splice(this.todos.indexOf(todo), 1)
todo.dispose()
}
}
// Domain object Todo.
export class Todo {
id = null // Unique id of this Todo, immutable.
completed = false
task = ""
author = null // Reference to an Author object (from the authorStore).
store = null
autoSave = true // Indicator for submitting changes in this Todo to the server.
saveHandler = null // Disposer of the side effect auto-saving this Todo (dispose).
constructor(store, id = uuid.v4()) {
makeAutoObservable(this, {
id: false,
store: false,
autoSave: false,
saveHandler: false,
dispose: false
})
this.store = store
this.id = id
this.saveHandler = reaction(
() => this.asJson, // Observe everything that is used in the JSON.
json => {
// If autoSave is true, send JSON to the server.
if (this.autoSave) {
this.store.transportLayer.saveTodo(json)
}
}
)
}
// Remove this Todo from the client and the server.
delete() {
this.store.transportLayer.deleteTodo(this.id)
this.store.removeTodo(this)
}
get asJson() {
return {
id: this.id,
completed: this.completed,
task: this.task,
authorId: this.author ? this.author.id : null
}
}
// Update this Todo with information from the server.
updateFromJson(json) {
this.autoSave = false // Prevent sending of our changes back to the server.
this.completed = json.completed
this.task = json.task
this.author = this.store.authorStore.resolveAuthor(json.authorId)
this.autoSave = true
}
// Clean up the observer.
dispose() {
this.saveHandler()
}
}
UIストア
ui-state-store は、アプリケーションに非常に特有であることが多く、通常は非常にシンプルでもあります。このストアには通常、多くのロジックは含まれていませんが、UIに関する多くの疎結合な情報を格納します。これは理想的です。なぜなら、ほとんどのアプリケーションは開発プロセス中にUI状態を頻繁に変更するためです。
UIストアに通常含まれるもの
- セッション情報
- アプリケーションのロードの進捗状況に関する情報
- バックエンドに格納されない情報
- グローバルにUIに影響を与える情報
- ウィンドウのサイズ
- アクセシビリティ情報
- 現在の言語
- 現在アクティブなテーマ
- 複数の、さらに関連のないコンポーネントに影響を与えるUI状態
- 現在の選択
- ツールバーなどの可視性
- ウィザードの状態
- グローバルオーバーレイの状態
これらの情報は、特定のコンポーネント(たとえば、ツールバーの可視性)の内部状態として開始することがよくありますが、しばらくすると、アプリケーションの他の場所でこの情報が必要になることがわかります。このような場合、プレーンなReactアプリケーションで行うように、コンポーネントツリーの上方に状態をプッシュする代わりに、その状態をui-state-store に移動するだけです。
アイソモーフィックアプリケーションの場合、健全なデフォルト値を持つストアのスタブ実装を提供して、すべてのコンポーネントが期待どおりにレンダリングされるようにすることもできます。Reactコンテキストとして渡すことで、ui-state-store をアプリケーション全体に配布できます。
ストアの例(ES6構文を使用)
import { makeAutoObservable, observable, computed } from "mobx"
export class UiState {
language = "en_US"
pendingRequestCount = 0
// .struct makes sure observer won't be signaled unless the
// dimensions object changed in a deepEqual manner.
windowDimensions = {
width: window.innerWidth,
height: window.innerHeight
}
constructor() {
makeAutoObservable(this, { windowDimensions: observable.struct })
window.onresize = () => {
this.windowDimensions = getWindowDimensions()
}
}
get appIsInSync() {
return this.pendingRequestCount === 0
}
}
複数のストアの組み合わせ
よくある質問として、シングルトンを使用せずに複数のストアを組み合わせる方法があります。それらはどのように互いに認識するのでしょうか?
効果的なパターンは、すべてのストアをインスタンス化し、参照を共有するRootStore
を作成することです。このパターンの利点は次のとおりです。
- 設定が簡単です。
- 強い型付けをうまくサポートします。
- ルートストアをインスタンス化するだけで済むため、複雑な単体テストが容易になります。
例
class RootStore {
constructor() {
this.userStore = new UserStore(this)
this.todoStore = new TodoStore(this)
}
}
class UserStore {
constructor(rootStore) {
this.rootStore = rootStore
}
getTodos(user) {
// Access todoStore through the root store.
return this.rootStore.todoStore.todos.filter(todo => todo.author === user)
}
}
class TodoStore {
todos = []
rootStore
constructor(rootStore) {
makeAutoObservable(this)
this.rootStore = rootStore
}
}
Reactを使用する場合、このルートストアは通常、Reactコンテキストを使用してコンポーネントツリーに挿入されます。