Context API
For JSignal user code, the Context API’s primary use case is to avoiding prop drilling, but it is also used extensivley in JSignal’s internals. Put breifly, it is a utility for scoped, single-threaded dependency injection. If that sentance didn’t make any sense, don’t worry; this section will try to provide an intuitive understanding so users are well equipped when working with it.
Also for reference, Java currently has an Scoped Values Preview API JEP 446 that is quite similar in functionality. Though it seems to have the additional aim of having strong integration with Java’s not yet stabalized structured concurrency features.
Example
It’s helpful to demonstrate the API with a simple example:
When the printContent
method calls contentContext.use()
it is implicity declaring a dependency on that context object. When called normally it will simply print contentContext
’s default value, but the Context API give users a way to inject any string value by creating a scope. In the second invocation of printContent
from the example, a scope with a new value is created, and the use
method returns the new value instead of the default. It is important to note that the second invocation could be deeply nested within other method calls, but as long as it is called in the scope of the lambda passed to provide
, use
will return the new value.
For those curious as to how signals and effects are implemented, the following simplified code is demonstrative how contexts are used in JSignal.
Provider
To fully understand the Context API, it is neccessary to understand the Provider
class. This class is a generic heterogenous map for Context
values. For reference, this pattern is well documented in the famous book Effective Java by Joshuia Bloch. The main difference here is that Provider
uses a persistent data structure (HAMT to be specific), so any instance of it is immutable and thread-safe; though, it should be noted that no garuntees are made about the thread-saftey of data it contains. Context
instances can essentially be thought of as typed key plus default value.
When the use
method of Context
is called, under the hood, it gets the current thread’s Provider
from a global ThreadLocal
instance then finds it’s value in the map (or returns the default if it is not found). New values are “injected” by adding/overwriting values in the current thread’s Provider
instance. To be more clear though, since Provider
is persistent, adding new values means a new Provider
instance is created and swapped with the current ThreadLocal
instance.
The persistence is important because it means that every Provider
instance is a context state snapshot that can be stored and loaded again at a later time. In fact, this is exactly what effects do. At creation, when they are initially run, they store the current Provider
; upon being reexecuted, they load that Provider
, so it’s logic always gets the same injected values. Building off of the last example, here is a simplified Effect
class: