Skip to content
Vincle

Loading…

    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.

    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.

    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:

    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.