Context Service

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

Status

This RFC was partially rejected during implementation due to the intersection with Wire Reform (#14). Parts of it are now implemented via Wire Reform, and the rest is still possible. In other words, the implementations details were discarded, while the context feature remains available.

Motivation

In a web component, data is passed top-down (host to child elements on its shadowRoot) via attributes, but this can be cumbersome for certain types of attributes (e.g. UI theme) that are required by many components within an application. Context provides a way to share values like these between components without having to explicitly pass a prop through every level of the tree.

Goals

The primary goal of this RFC is to define how to consume context, guarantee the right ergonomics and semantics for the consumer code while the provider can be an experimental API.

As a secondary goal is to provide ways for the Lightning Platform to control who can provide new context values, as a way to gate this feature.

A third goal is to support the provision of context values on any element, whether it is LWC or not.

Prior Art

Detailed design

Important Notes:

Proposal: Context Consumer

This proposal provides a high-level API (an abstraction layer) for authors to consume a context value at any level in the DOM flatten tree. This must be a future-proof API, and must be easy to use and easy to maintain. Here is an example:

import { LightningElement } from 'lwc';
import XFooContextElement from 'x/fooContext';
import XBarContextElement from 'x/barContext';

export default class MyComponent extends LightningElement {
    // context value provided by <x-foo-context> wired into a field
    @wire(XFooContextElement.Provider) a;
    // context value provided by <x-bar-context> wired into a method
    @wire(XBarContextElement.Provider) someMethod(contextValue) {
        // contextValue available after connecting
    }
}

Pros:

Cons:

Note: This is very similar to how React does it, but extending it to access multiple contexts.

Note: since @wire supports wiring to a method, it will work the same way for context values.

Proposal: Context Provider

This proposal provides a way for authors to provide a context value at any level in the flattened DOM tree that can be consumed by any node in the subtree.

Defining a new Context Provider

The definition of a new context is equivalent to creation of a component that must extend LightningContextElement, e.g.:

import LightningContextElement from 'lightning/context';

export default class ThemeContextElement extends LightningContextElement {
    @api set theme(data) {
        this._theme = data;
        this.setContext({ data });
    }
    get theme() {
        return this._theme;
    }
}

Where LightningContextElement component is provided by the lightning namespace. This class is intended to be extended and provides one protected method setContext(newValue) to interact with the underlying implementation that takes care of provisioning the context value to any node consuming this new context.

Additionally, this class provides a protected protocol to define the default value to be set for the context in case no provider is found. This can be done via static getDefaultContext() { return defaultValue; } method in the subclass. This is optional, and by default, it will return undefined.

Note: No html file is required, unless the author want to customize the UI of the context provider.

This class also provides a static accessor called Provider, which is intended to be used for the consumer to wire to the provided context, e.g.:

import { LightningElement } from 'lwc';
import ThemeContextElement from 'x/themeContext';

// Consumer
export default class XBar extends LightningElement {
    @wire(ThemeContextElement.Provider) x;
}

Providing a context value

Setting a context value must be an operation involving a custom element. Any element in the light DOM subtree of that element or their descendent, can consume the value set for a context.

To provide a new context value, you must use the declarative form via an HTML template. Any structure that involves <x-theme-context> and <x-bar> components can share the theme value, e.g.:

<template>
    <x-theme-context theme="dark">
        <x-bar></x-bar>
    </x-theme-context>
</template>

It means that <x-bar> or any of its descendants can wire to ThemeContextElement.Provider to obtain the theme value.

Chaining context value throughout the DOM

Sometimes you want to have a hierarchical context value, where intermediate context element provider can be linked to another parent, and propagate pieces of the value, e.g.:

import LightningContextElement from 'lightning/context';

export default class DepthContextElement extends LightningContextElement {
    @wire(DepthContextElement.Provider) onParentContext(value) {
        this.setContext(value + 1);
    }

    static getDefaultValue() {
        return 0;
    }
}

The example above is very trivial, but illustrate how to collect the outer context value, and propagate a new one to its flattened DOM subtree.

Semantics and Invariants

It is the responsibility of the context provider to define the proper signals to its consumers. This is our recommendation for context element authors:

Implementation Details

In order to implement LightningContextElement, it requires a reform on the wire protocol. This reform has two folds:

  1. We need to introduce a new event that can be dispatched by a wire adapter to create a side channel between the eventTarget with a context provider installed on an element in the composed path. The new event is implemented via a new constructor called LinkContextEvent, and it expects a unique token, and a callback to finalize the linking process. E.g.:
import { register, ValueChangedEvent, LinkContextEvent } from 'wire-service';

const adapter = Symbol('ContextProvider');
register(adapter, (eventTarget) => {
    let unsubscribeCallback;

    function callback(data, unsubscribe) {
        // provider calls this function at any given time
        eventTarget.dispatchEvent(new ValueChangedEvent(data));
        unsubscribeCallback = unsubscribe;
    }

    eventTarget.addEventListener('connect', () => {
        // attempt to link to a context provider identified by token
        const event = new LinkContextEvent(token, callback);
        eventTarget.dispatchEvent(event);
    });

    eventTarget.addEventListener('disconnect', () => {
        if (unsubscribeCallback !== undefined) {
            unsubscribeCallback();
        }
    });
});
  1. The existing, and hacky way, to implement context with wire via an arbitrary event name "WireContextEvent", must be removed. This cannot happen right now, we should move existing consumers to the new API.

Adoption strategy

This is a brand new feature, we just need to document it.

How we teach this

Unresolved questions

undefined