Solid

Use Lunora with SolidJS — a provider, live createQuery/createMutation primitives, and the SSR-seed → live reactive-loader handoff.

Last updated:

@lunora/solid is the SolidJS adapter, an idiomatic layer over @lunora/client. Solid's fine-grained signals map directly onto Lunora's per-subscription deltas, so a live query is a signal the socket writes to: only the components that read the accessor re-render.

This page is the task-oriented guide; for the full reference see @lunora/solid.

Preview. The Solid adapter exposes the same hydrate-then-subscribe handoff as React, but it is preview maturity: not yet proven end-to-end against a running app. Solid lands first after React because its signals map most directly onto Lunora deltas. See Bring your framework.

Install

pnpm add @lunora/solid
npm install @lunora/solid
yarn add @lunora/solid
bun add @lunora/solid

Provide the client

Wrap the tree in <LunoraProvider client={...}> (required at the root). Read it anywhere below with useLunora().

src/index.tsx
import { LunoraClient } from "lunorash/client";
import { LunoraProvider } from "@lunora/solid";
import { render } from "solid-js/web";

import App from "./App";

const client = new LunoraClient({ url: window.location.origin });

render(
    () => (
        <LunoraProvider client={client}>
            <App />
        </LunoraProvider>
    ),
    document.getElementById("root")!,
);

Live queries

createQuery(fn, args) subscribes and returns a reactive accessor (undefined until the first frame, then updated on every delta). Pass args as a plain value or an accessor; an accessor re-subscribes when it changes. Pass "skip" to short-circuit.

import { createQuery } from "@lunora/solid";
import { For, createSignal } from "solid-js";

import { api } from "./lunora/_generated/api";

function Messages() {
    const [channelId, setChannelId] = createSignal("general");

    // Accessor args: changing `channelId()` re-subscribes.
    const messages = createQuery(api.messages.list, () => ({ channelId: channelId() }));

    return <For each={messages() ?? []}>{(m) => <li>{m.text}</li>}</For>;
}

Pass { shardKey } in options to target a specific shard when the function is .shardBy(...)-partitioned. The subscription tears down when the owning reactive scope is disposed.

Mutations

createMutation(fn) returns a MutationHandle of accessors: { data, error, pending, mutate, reset }. pending is ref-counted, and the mutation engages the client's offline queue when the socket is down. You pass optimistic updates per call.

import { createMutation } from "@lunora/solid";

import { api } from "./lunora/_generated/api";

function Composer(props: { channelId: string }) {
    const send = createMutation(api.messages.send);

    return (
        <button
            disabled={send.pending()}
            onClick={() =>
                void send.mutate(
                    { channelId: props.channelId, text: "hi" },
                    {
                        optimisticUpdate: (store) => {
                            const current = store.getQuery(api.messages.list, { channelId: props.channelId }) ?? [];
                            store.setQuery(api.messages.list, { channelId: props.channelId }, [...current, { _id: "tmp", text: "hi" }]);
                        },
                    },
                )
            }
        >
            Send
        </button>
    );
}

Reactive loaders

Run the query on the server with the socket-free @lunora/solid/server entry inside a SolidStart route loader, hand the serializable Preloaded token to the client, and hydratePreloaded seeds the accessor synchronously. The first read returns the server value with no loading flash and no Suspense fallback, then the live subscription attaches after mount.

server (SolidStart loader)
import { createServerClient, preloadQuery } from "@lunora/solid/server";

import { api } from "./lunora/_generated/api";

// Per request — never reuse a client across requests (token leakage).
const client = createServerClient({ url: process.env.LUNORA_URL!, token });
const preloaded = await preloadQuery(client, api.posts.list, {});
import { hydratePreloaded } from "@lunora/solid";
import type { Preloaded } from "@lunora/solid";
import { For } from "solid-js";

function Posts(props: { preloaded: Preloaded<Array<{ _id: string; title: string }>> }) {
    // Seeded from SSR on the first read, then live.
    const posts = hydratePreloaded(props.preloaded);

    return <For each={posts()}>{(p) => <li>{p.title}</li>}</For>;
}

Effects do not run during SSR, so the subscription is strictly client-side: the seed is the only value the server render sees. See Reactive loaders for the full handoff.

See also