Context
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 aware of it.
Lit's context implementation is part of Lit Labs and available in the @lit-labs/context package:
npm i @lit-labs/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.
Example
Using context involves a context object (sometimes called a key), a provider and a consumer, which communicate using the context object.
Context definition (logger-context.ts)
import { createContext } from "@lit-labs/context";
import type { Logger } from "my-logging-library";
export type { Logger } from "my-logging-library";
export const loggerContext = createContext<Logger>("logger");
Provider
import { LitElement, property, html } from "lit";
import { provide } from "@lit-labs/context";
import { Logger } from "my-logging-library";
import { loggerContext } from "./logger-context.js";
@customElement("my-app")
class MyApp extends LitElement {
@provide({ context: loggerContext })
logger = new Logger();
render() {
return html`...`;
}
}
Consumer
import { LitElement, property } from "lit";
import { consume } from "@lit-labs/context";
import { type Logger, loggerContext } from "./logger-context.js";
export class MyElement extends LitElement {
@consume({ context: loggerContext })
@property({ attribute: false })
public logger?: Logger;
private doThing() {
this.logger?.log("A thing was done");
}
}
Key Concepts
Context Protocol
Lit's context is based on the Context Community Protocol by the W3C's Web Components Community Group. 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-labs/context
implements this event-based protocol and makes it available via a few reactive controllers and decorators.
Context Objects
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.
Providers
Providers are usually elements (but can be any event handler code) that provide data for specific context keys.
Consumers
Consumers request data for specific context keys.
Subscriptions
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.
Usage
Defining a context
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:
export const myContext = createContext(Symbol("my-context"));
It is recommended to put context objects in their own module so that they're importable independent of specific providers and consumers.
Context type-checking
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. In case of a mistake like this:
const myContext = createContext<Logger>(Symbol("logger"));
class MyElement extends LitElement {
@provide({ context: myContext })
name: string;
}
TypeScript will warn that the type string
is not assignable to the type Logger
.
Context equality
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 separate createContext()
calls to refer to the same context, then use a key that will be equal under strict equality like a string:
// true
createContext("my-context") === createContext("my-context");
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.
Providing context
There are two ways in @lit-labs/context
to provide a context value: the ContextProvider
controller and the @provide()
decorator.
@provide()
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-labs/context";
import { myContext, MyData } from "./my-context.js";
class MyApp extends LitElement {
@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:
html`<my-provider-element .myData="${someData}">`</my-provider-element>