---
title: "Firelight.Context"
description: "Share a value with every component in a subtree, without passing it through each layer. F# bindings for Lit's @lit/context."
---

# Firelight.Context

Implicitly share values with any descendant component in the DOM tree, without prop drilling.

- [Lit docs: Context](https://lit.dev/docs/data/context/)
- [NuGet 0.3.0](https://www.nuget.org/packages/Firelight.Context/0.3.0)
- [Source](https://github.com/roboz0r/Firelight/tree/main/src/Firelight.Context)

## Why use it

Some values are needed deep in the component tree: the current theme, the active user, or an
Elmish `dispatch` function. Passing them down as properties forces every component in between to
accept and forward values it doesn't use.

Context solves this with an event-based protocol that runs through the DOM:

- **No intermediate plumbing:** a provider component declares a value, and any descendant component
  can request it. The components in between don't know the value passes through them.
- **Subtree scoping:** unlike global stores or signals, context follows the DOM hierarchy. Nest
  providers to scope a value locally, for example a dark theme for a single card while the rest of
  the page stays light.
- **Typed keys:** a Firelight context key is branded with the value's F# type, so a consumer that
  requests a `Theme` receives a `Theme`. When the provider's value changes, subscribed consumers
  re-render.

## Example

The provider owns the theme and shows its children through a slot. The badges are nested inside
plain `<div>`s, and nothing passes the theme to them.

```fsharp
module Snippets.ThemeContext

open Fable.Core
open Fable.Core.JsInterop
open Firelight
open Firelight.Context
open type Firelight.Lit

type Theme =
    | Ember
    | Ocean

// A context is a symbol branded with the type of value it carries.
type ThemeContext =
    inherit Context<Theme>
    inherit symbol

let themeContext: ThemeContext = LitContext.createContext (JS.Symbol "theme")

/// Provides the current theme to everything inside it, however deeply nested.
[<AttachMembers>]
type ThemeProvider() =
    inherit LitElement()

    let mutable theme = Ember
    let provider = ContextProvider(jsThis, ContextProvider.Options(themeContext, theme))

    static member styles =
        css
            $$"""
        :host { display: grid; gap: 1rem; justify-items: start; }
        button { font: inherit; padding: 0.4rem 0.9rem; cursor: pointer; }
        """

    member this.Toggle() =
        theme <- if theme = Ember then Ocean else Ember
        provider.setValue theme
        this.requestUpdate ()

    override this.render() =
        html
            $"""
        <button @click={fun _ -> this.Toggle()}>Switch theme</button>
        <slot></slot>"""

/// Reads the theme from the nearest provider. Nothing in between passes it along.
[<AttachMembers>]
type ThemedBadge() =
    inherit LitElement()

    let theme =
        ContextConsumer(jsThis, ContextConsumer.Options(themeContext, subscribe = true))

    static member styles =
        css
            $$"""
        span { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 1rem; color: white; }
        .Ember { background: #c2410c; }
        .Ocean { background: #1971c2; }
        """

    override _.render() =
        let name = string (theme.value |> Option.defaultValue Ember)
        html $"""<span class={name}>{name}</span>"""

defineElement<ThemeProvider> "my-theme-provider"
defineElement<ThemedBadge> "my-themed-badge"
```

```html
<my-theme-provider>
  <div class="nested">
    <div class="nested"><my-themed-badge></my-themed-badge></div>
    <my-themed-badge></my-themed-badge>
  </div>
</my-theme-provider>
```

The [Todo demo](https://roboz0r.github.io/Firelight/demos/todo/) uses the same pattern to share its Elmish state and `dispatch` with each item.

## Providers that load later

A consumer asks for its value once, when it connects. If the provider's element isn't defined yet,
because its module loads later or lazily, nobody answers and the consumer waits for good. A
`ContextRoot` attached to the page keeps those requests and sends them again when a provider
connects. It keeps only requests from consumers with `subscribe = true`.

Here the consumer connects inside a `<my-late-provider>` that isn't defined yet. Click the button to
define it:

```fsharp
module Snippets.LateProvider

open Fable.Core
open Fable.Core.JsInterop
open Browser
open Firelight
open Firelight.Context
open type Firelight.Lit

type GreetingContext =
    inherit Context<string>
    inherit symbol

let greetingContext: GreetingContext =
    LitContext.createContext (JS.Symbol "greeting")

// Keeps the requests nobody answered, and sends them again when a provider appears.
ContextRoot().attach document.body

/// Asks for the greeting as soon as it connects, before any provider exists.
[<AttachMembers>]
type LateConsumer() =
    inherit LitElement()

    // ContextRoot only keeps requests that subscribe.
    let greeting =
        ContextConsumer(jsThis, ContextConsumer.Options(greetingContext, subscribe = true))

    override _.render() =
        match greeting.value with
        | Some text -> html $"<p>{text}</p>"
        | None -> html $"<p>Waiting for a provider…</p>"

[<AttachMembers>]
type LateProvider() =
    inherit LitElement()

    let provider =
        ContextProvider(jsThis, ContextProvider.Options(greetingContext, "Hello from the provider"))

    override _.render() = html $"<slot></slot>"

/// Defines <my-late-provider> on click, so the provider upgrades after its consumer asked.
[<AttachMembers>]
type ProviderLoader() =
    inherit LitElement()

    let mutable loaded = false

    member this.Load() =
        defineElement<LateProvider> "my-late-provider"
        loaded <- true
        this.requestUpdate ()

    override this.render() =
        html $"""<button ?disabled={loaded} @click={fun _ -> this.Load()}>Define the provider</button>"""

defineElement<LateConsumer> "my-late-consumer"
defineElement<ProviderLoader> "my-provider-loader"
```

```html
<my-provider-loader></my-provider-loader>
<my-late-provider data-defined-later><my-late-consumer></my-late-consumer></my-late-provider>
```

Attach the root once, before the consumers connect, to an element above both: `document.body`,
or your app's root element.

## Install

```sh
dotnet add package Firelight.Context
npm install @lit/context
```
