---
title: Context & Scope
---

# Context & Scope

Vincle has two ways to share values. **Tree Context** passes a value to
descendants of a JSX Provider. **Scope** holds mutable state for one async
execution, such as request metadata or Flow orchestration.

## Tree Context

`createContext(default)` creates a context; `useContext(context)` reads the
value of the nearest Provider above, or the default outside any Provider.

```tsx
import { createContext, useContext, renderToString } from "@vincle/core";

const Theme = createContext<"light" | "dark">("light");

function ThemedBox({ children }: { children: string }) {
  const theme = useContext(Theme);
  return <div class={theme === "dark" ? "dark" : "light"}>{children}</div>;
}

const html = await renderToString(
  <Theme.Provider value="dark">
    <ThemedBox>Hello</ThemedBox>
  </Theme.Provider>,
);
```

A Provider needs no `Scope.with`. Its value holds across `await` and across
deferred Flow fragments, and concurrent renders never share it. The shape
matches React, Preact and Hono, but their context objects are not
interchangeable with vincle's.

## Scope

`Scope.key<T>(name)` creates a named, typed token. `Scope.with(fn)` runs `fn` in
an isolated async scope, where `Scope.set(key, value)` writes and
`Scope.get(key)` reads the last value written, so a later sibling reads what an
earlier component set. `get` throws outside a scope or when the key has not
been set.

```tsx
import { Scope, renderToString } from "@vincle/core";

declare function getSession(req: Request): Promise<{ userId: string }>;
const Request = Scope.key<{ userId: string; locale: string }>("app:request");
const App = () => <main>{Scope.get(Request).userId}</main>;

async function handleRequest(req: Request): Promise<Response> {
  const session = await getSession(req);
  const html = await Scope.with(() => {
    Scope.set(Request, {
      userId: session.userId,
      locale: req.headers.get("Accept-Language") ?? "en",
    });
    return renderToString(<App />);
  });
  return new Response(html, { headers: { "Content-Type": "text/html" } });
}
```

`Scope.snapshot()` copies the current map; pass the copy as the second argument
of `Scope.with` to seed a child scope:

```tsx
const childHtml = await Scope.with(async () => {
  Scope.set(Theme, "dark");
  return Scope.with(() => renderToString(<ChildPage />), Scope.snapshot());
});
```

## Runtime support

Both stores use `globalThis.AsyncLocalStorage` or `node:async_hooks`. Without
either, they warn and support one render at a time (async included): a second
render entering a Provider or `Scope.with` while the first is in flight is
refused. A concurrent render that reads a context without entering one may see
the in-flight render's values. Enable `AsyncLocalStorage` for concurrent
rendering.
