Context is a way of making data available to entire component subtrees without having to manually bind properties to every component. The data is "contextually" available, such that ancestor elements in between a provider of data and consumer of data aren't even aware of it.
Lit's context implementation is available in the @lit/context package:
npm i @lit/context
Context is useful for data that needs to be consumed by a wide variety and large number of components - things like an app's data store, the current user, a UI theme - or when data-binding isn't an option, such as when an element needs to provide data to its light DOM children.
Context is very similar to React's Context, or to dependency injection systems like Angular's, with some important differences that make Context work with the dynamic nature of the DOM, and enable interoperability across different web components libraries, frameworks and plain JavaScript.
This protocol enables interoperability between elements (or even non-element code) regardless of how they were built. Via the context protocol, a Lit-based element can provide data to a consumer not built with Lit, or vice versa.
The Context Protocol is based on DOM events. A consumer fires a context-request event that carries the context key that it wants, and any element above it can listen for the context-request event and provide data for that context key.
@lit/context implements this event-based protocol and makes it available via a few reactive controllers and decorators.
Contexts are identified by context objects or context keys. They are objects that represent some potential data to be shared by the context object identity. You can think of them as similar to Map keys.
When a consumer requests data for a context, it can tell the provider that it wants to subscribe to changes in the context. If the provider has new data, the consumer will be notified and can automatically update.
Every usage of context must have a context object to coordinate the data request. This context object represents the identity and type of data that is provided.
Context objects are created with the createContext() function:
createContext() takes any value and returns it directly. In TypeScript, the value is cast to a typed Context object, which carries the type of the context value with it.
Context objects are used by providers to match a context request event to a value. Contexts are compared with strict equality (===), so a provider will only handle a context request if its context key equals the context key of the request.
This means that there are two main ways to create a context object:
With a value that is globally unique, like an object ({}) or symbol (Symbol())
With a value that is not globally unique, so that it can be equal under strict equality, like a string ('logger') or global symbol (Symbol.for('logger')).
If you want two separatecreateContext() calls to refer to the same context, then use a key that will be equal under strict equality like a string:
Beware though that two modules in your app could use the same context key to refer to different objects. To avoid unintended collisions you may want to use a relatively unique string, e.g. like 'console-logger' instead of 'logger'.
Usually it's best to use a globally unique context object. Symbols are one of the easiest ways to do this.
The @provide() decorator is the easiest way to provide a value if you're using decorators. It creates a ContextProvider controller for you.
Decorate a property with @provide() and give it the context key:
import {LitElement, html} from'lit';
import {property} from'lit/decorators.js';
import {provide} from'@lit/context';
import {myContext, MyData} from'./my-context.js';
classMyAppextendsLitElement {
@provide({context: myContext})
myData: MyData;
}
You can make the property also a reactive property with @property() or @state() so that setting it will update the provider element as well as context consumers.
@provide({context: myContext})
@property({attribute: false})
myData: MyData;
Context properties are often intended to be private. You can make private properties reactive with @state():
@provide({context: myContext})
@state()
private _myData: MyData;
Making a context property public lets an element provide a public field to its child tree:
The @consume() decorator is the easiest way to consume a value if you're using decorators. It creates a ContextConsumer controller for you.
Decorate a property with @consume() and give it the context key:
import {LitElement, html} from'lit';
import {consume} from'@lit/context';
import {myContext, MyData} from'./my-context.js';
classMyElementextendsLitElement {
@consume({context: myContext})
myData: MyData;
}
When this element is connected to the document, it will automatically fire a context-request event, get a provided value, assign it to the property, and trigger an update of the element.
ContextConsumer is a reactive controller that manages dispatching the context-request event for you. The controller will cause the host element to update when new values are provided. The provided value is then available at the .value property of the controller.
The most common context use cases involve data that is global to a page and possibly only sparsely needed in components throughout the page. Without context it's possible that most or all components would need to accept and propagate reactive properties for the data.
App-global services, like loggers, analytics, data stores, can be provided by context. An advantage of context over importing from a common module are the late coupling and tree-scoping that context provides. Tests can easily provide mock services, or different parts of the page can be given different service instances.
Themes are sets of styles that apply to the entire page or entire subtrees within the page - exactly the kind of scope of data that context provides.
One way of building a theme system would be to define a Theme type that containers can provide that holds named styles. Elements that want to apply a theme can consume the theme object and look up styles by name. Custom theme reactive controllers can wrap ContextProvider and ContextConsumer to reduce boilerplate.
Context can be used to pass data from a parent to its light DOM children. Since the parent does usually not create the light DOM children, it cannot leverage template-based data-binding to pass data to them, but it can listen to and respond to context-request events.
For example, consider a code editor element with plugins for different language modes. You can make a plain HTML system for adding features using context:
In this case <code-editor> would provide an API for adding language modes via context, and plugin elements would consume that API and add themselves to the editor.
Sometimes reusable components will need to format data or URLs in an application-specific way. For example, a documentation viewer that renders a link to another item. The component will not know the URL space of the application.
In these cases the component can depend on a context-provided function that will apply the application-specific formatting to the data or link.
If you want two separate createContext() calls to referrer to the same context, then use a key that will by equal under strict equality like a string for Symbol.for():
If you want a context to be unique so that it's guaranteed to not collide with other contexts, use a key that's unique under strict equality, like a Symbol() or object.:
A property decorator that adds a ContextProvider controller to the component making it respond to any context-request events from its children consumer.
A ReactiveController which adds context provider behavior to a custom element by listening to context-request events.
Import:
import {ContextProvider} from'@lit/context';
Constructor:
ContextProvider(
host: ReactiveElement,
options: {
context: T,
initialValue?: ContextType<T>
}
)
Members
setValue(v: T, force = false): void
Sets the value provided, and notifies any subscribed consumers of the new value if the value changed. force causes a notification even if the value didn't change, which can be useful if an object had a deep property change.
When the host element is connected to the document it will emit a context-request event with its context key. When the context request is satisfied the controller will invoke the callback, if present, and trigger a host update so it can respond to the new value.
It will also call the dispose method given by the provider when the host element is disconnected.
A ContextRoot can be used to gather unsatisfied context requests and re-dispatch them when new providers which satisfy matching context keys are available. This allows providers to be added to a DOM tree, or upgraded, after the consumers.
Import:
import {ContextRoot} from'@lit/context';
Constructor:
ContextRoot()
Members
attach(element: HTMLElement): void
Attaches the ContextRoot to this element and starts listening to context-request events.
detach(element: HTMLElement): void
Detaches the ContextRoot from this element, stops listening to context-request events.