Skip to main content

Contexts

Overview

Contexts serve as a way to share contextual/environmental data deeply across a tree without having to know the exact structure of such tree. Think of it as a dependency injection system.

For example, imagine a state where some children need to know the current username to perform certain operations. We could make the children use getRoot to access the username, but that means we need a root for unit testing any of the children, therefore, we created a dependency from a child to one of its parents.

With contexts we could make it like this:

const usernameCtx = createContext<string>()

@model("MyApp/SomeParent")
class SomeParent extends Model({
username: prop<string>(),
// ...
}) {
onInit() {
usernameCtx.setComputed(this, () => this.username)
}
}

@model("MyApp/SomeDeepChild")
class SomeDeepChild extends Model({...}) {
@modelAction
someActionThatRequiresUsername() {
const username = usernameCtx.get(this)
// ...
}

@computed
get someComputedThatRequiresUsername() {
return usernameCtx.get(this) + " is awesome!"
}
}

With this code, whenever the child is attached to the parent the username will be fetched from the parent. The fact that we can set the context value at any point of the tree also makes it easier to unit test the child model in isolation:

const child = new SomeDeepChild(...)
usernameCtx.set(child, "RandomUsername")
expect(child.someComputedThatRequiresUsername).toBe("RandomUsername is awesome!")

When using createContext a default value can be also provided (e.g. const userCtx = createContext("defaultUsername")), which will be used when no node higher in the tree provides a value.

The returned context object has the following methods:

  • getDefault() - Gets the default context value.
  • setDefault(value) - Sets the (static) default context value.
  • setDefaultComputed(() => value) - Sets the (computed) default context value.
  • get(node) - Gets the context value for a given node (recursing up in the tree until a node has a set value or the default if none is set). Usually called in actions and computed getters. This value is reactive/observable, so never cache this value since it might get stale.
  • set(node, value) - Sets the (static) value a node will provide for itself and its children. Usually called in onInit.
  • setComputed(node, () => value) - Sets the (computed) value a node will provide for itself and its children. Usually called in onInit.
  • unset(node) - Make the node no longer provide a context value.
  • getProviderNode(node) - Gets the node that will provide the value, or undefined if it will come from the default value.
  • apply(fn, value) - Applies a value override while the given function is running and, if a node is returned, sets the node as a provider of the value.
  • applyComputed(fn, () => value) - Applies a computed value override while the given function is running and, if a node is returned, sets the node as a provider of the computed value.

In particular, apply is more useful than it may seem, since it allows you to pass volatile (non-property) data to your nodes at construction time (be it through new, fromSnapshot, clone, toTreeNode, etc.). For example:

const envCtx = createContext(0)

@model(...)
class M extends Model({...}) {
onInit() {
const value = envCtx.get(this)
}

get value() {
return envCtx.get(this)
}
}

const m = envCtx.apply(() => new M({...}), 9000)
// onInit's "value" will be 9000
m.value // this will also be 9000