Contexts
Overview
Contexts let you share environment or dependency data across a tree without coupling a model to the tree's exact shape. You can think of them as dependency injection for tree nodes.
For example, imagine that some children need the current username. They could call getRoot, but that would couple them to a specific parent structure and force every unit test to construct a suitable root store.
With a context, the parent can provide the value without the child knowing where it came from:
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)
console.log(`running as ${username}`)
}
@computed
get someComputedThatRequiresUsername() {
return usernameCtx.get(this) + " is awesome!"
}
}
Whenever the child is attached below this parent, it resolves the username from the nearest provider. Because a value can be provided at any node, the child is also easy to test in isolation:
const child = new SomeDeepChild({})
usernameCtx.set(child, "RandomUsername")
expect(child.someComputedThatRequiresUsername).toBe("RandomUsername is awesome!")
You can pass a default value to createContext (for example, const userCtx = createContext("defaultUsername")). The context uses it when neither the node nor any ancestor 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 inonInit.setComputed(node, () => value)- Sets the (computed) value a node will provide for itself and its children. Usually called inonInit.unset(node)- Make the node no longer provide a context value.getProviderNode(node)- Gets the node that provides the value, orundefinedwhen the default value is used.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 lets you provide volatile (non-property) data while constructing nodes through new, fromSnapshot, clone, toTreeNode, and similar APIs.
For example:
const envCtx = createContext(0)
@model("MyApp/M")
class M extends Model({
title: prop("demo"),
}) {
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