Skip to main content

Root Stores

Overview

An application usually has one or more store objects that represent its current live state. These objects are commonly called root stores.

In mobx-keystone, a root store is a registered tree node: either a model instance or a plain object/array converted with toTreeNode. Registering a root store is optional, but it provides a clear boundary for your live application state:

  • Models can use onAttachedToRootStore(rootStore) to start and dispose side effects as they enter or leave the live tree.
  • getRootStore(node) can resolve the registered application root from any descendant.
  • isRootStore(node) can distinguish a registered root from an ordinary detached tree.

You can think of a tree node marked as a root store as the "live tree" of your application state, meaning that any nodes attached to a root store are actually part of your running application, rather than a transient instance that might or might not end up as part of your actual application state.

Register and unregister a tree node as follows:

// given `myTodoList` is a model instance of a todo list ...
registerRootStore(myTodoList)

// unregistering
unregisterRootStore(myTodoList)

If you need to check whether a node is currently registered as a root store, use isRootStore(node).

onAttachedToRootStore

When a root is registered, onAttachedToRootStore runs for the root model and each descendant model. It also runs for models attached later. The hook may return a disposer, which runs when that model leaves the root-store tree or when the root itself is unregistered.

This makes the hook a natural place to register effects such as MobX reaction or when, and its disposer the matching place to clean them up.

Practical example

As a practical example, say that you have some kind of application user preferences that have to be saved to / loaded from local storage, but we also want to use the same model in a form to edit them.

First we need to define a model for the user preferences, as well as its desired side effects when it is part of the actual application state (when it is part of a root store tree) ...

type Theme = "light" | "dark"

@model("myApp/UserPreferences")
class UserPreferences extends Model({ theme: prop<Theme>().withSetter() }) {
// once we are part of the root store ...
onAttachedToRootStore() {
// every time the snapshot of the configuration changes
const reactionDisposer = reaction(
() => getSnapshot(this),
(sn) => {
// save the config to local storage
localStorage.setItem("myPreferences", JSON.stringify(sn))
},
{
// also run the reaction the first time
fireImmediately: true,
}
)

// when the model is no longer part of the root store stop saving
return () => {
reactionDisposer()
}
}
}

... we also need to model the root store of our application ...

@model("myApp/RootStore")
class RootStore extends Model({
userPreferences: prop<UserPreferences>().withSetter(),
}) {}

... then we will need some code to initialize our application, loading the preferences already stored in local storage ...

const myPreferencesRaw = localStorage.getItem("myPreferences")

// this will create a `UserPreferences` model instance, but won't save any changes yet
// since it is not yet part of a root store
// this means we can manipulate it without fear of overwriting the
// config in local storage
const myPreferences = myPreferencesRaw
? fromSnapshot(UserPreferences, JSON.parse(myPreferencesRaw))
: new UserPreferences({ theme: "light" })

... and finally creating the root store itself with the initial data ...

const myRootStore = new RootStore({ userPreferences: myPreferences })

// after this next function is called, `myPreferences` will become part of a root store
// and therefore start saving now and on changes
registerRootStore(myRootStore)
// the preferences get saved ...

Now we would like to have a form which will be able to edit a copy of the current user preferences ...

// we make a clone of the current preferences
const formPreferences = clone(myRootStore.userPreferences)
// since the clone is outside the root store it WON'T be auto-saved

// the form eventually makes changes ...
formPreferences.setTheme("dark")
// but that's ok, it is not auto-saved since it is not part of a root store,
// therefore living "outside" the actual application state

... but it should be saved once the save button is clicked:

myRootStore.setUserPreferences(formPreferences)

After that last line, the old preferences object (myPreferences) becomes detached from the root store tree, so it stops saving changes when its disposer runs. At the same time, the new preferences object (formPreferences) becomes part of the root store tree, runs the hook, and starts saving future changes.

This is why onAttachedToRootStore is such a good place to manage side effects.

Sharing contextual data

Although contexts are usually preferred for this case (see the contexts section), root stores can also hold global runtime environment data that should be shared across the application but does not need to be serialized.

In reactive code, prefer getRootStore over getRoot when you need the application root store. Detached nodes can make getRoot(node) resolve to node itself.

For example:

@model("myApp/RootStore")
class RootStore extends Model({
currentUserId: prop("user-1"),
}) {
env!: {
environment: "development" | "staging" | "production"
releaseChannel: "internal" | "public"
enableDebugTools: boolean
}
}

const rootStore = new RootStore({})
rootStore.env = {
environment: "development",
releaseChannel: "internal",
enableDebugTools: true,
}
registerRootStore(rootStore)

// then on another model
@model("myApp/Profile")
class Profile extends Model({}) {
get shouldShowInternalFeatures() {
const rootStore = getRootStore<RootStore>(this)
return rootStore?.env.releaseChannel === "internal"
}

onAttachedToRootStore(rootStore) {
if (rootStore.env.environment !== "production" && rootStore.env.enableDebugTools) {
console.log("debug tools enabled?", rootStore.env.enableDebugTools)
}
}
}

getRootStore(node) is also usually preferable to manually walking to the root and then checking it yourself, since it directly returns the registered root store or undefined.