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:
| Method | Description |
|---|---|
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 the tree until a node has a set value, or the default if none is set. Usually called in actions and computed getters. |
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) | Makes the node no longer provide a context value. |
getProviderNode(node) | Gets the node that provides the value, or undefined when 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. |
get(node)The value returned by get(node) is observable and can change, so read it where you need it instead of storing it.
apply and applyComputed return the callback result unchanged. Returning a model's $ data object does not make its model a context provider.
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