Every state source, in concert.
Symphony State is a lightweight orchestration layer for server caches, UI state, URL params, and browser persistence. It keeps your existing stores independent, then composes their updates into predictable flows with atomic transactions, derived sections, and observable reconciliation.
Modern apps blend server state (SWR/TanStack Query), local UI state, URL params, and client caches. Those sources drift, and race conditions appear.
Symphony State focuses on coordination:
- Orchestration, not ownership: keep each state source independent.
- Deterministic updates: staged commits resolve dependencies in a single wave.
- Smart reconciliation: establish precedence between server, cache, and optimistic UI.
- Observability: inspect which source is driving your UI at any time.
- Performance: local updates notify only subscribers for touched sections.
npm install @shiftbloom-studio/symphony-stateimport {
createConductor,
defineSection,
createAtomAdapter
} from "@shiftbloom-studio/symphony-state";
import { SymphonyProvider, useSection } from "@shiftbloom-studio/symphony-state/react";
const auth = defineSection({
key: "auth",
source: createAtomAdapter({ userId: null as string | null })
});
const conductor = createConductor({ sections: [auth] });
function AuthPanel() {
const authSection = useSection<{ userId: string | null }>("auth");
return (
<button onClick={() => authSection.set({ userId: "42" })}>
{authSection.value.userId ?? "Login"}
</button>
);
}
export function App() {
return (
<SymphonyProvider conductor={conductor}>
<AuthPanel />
</SymphonyProvider>
);
}The Conductor stitches independent sources together. Use the orchestrated adapter to define a "symphony" of sources that resolves into a single view.
import {
createOrchestratedAdapter,
defineSection,
createAtomAdapter,
createExternalStoreAdapter
} from "@shiftbloom-studio/symphony-state";
const serverCache = createExternalStoreAdapter(serverCacheStore);
const localDraft = createAtomAdapter({ title: "", body: "" });
const postSection = defineSection({
key: "post",
source: createOrchestratedAdapter({
instruments: [
{ id: "server", source: serverCache, priority: 1, role: "server", staleAfterMs: 30_000 },
{ id: "draft", source: localDraft, priority: 2, role: "optimistic" }
],
writeTo: "draft",
optimistic: true
})
});Precedence is decided by priority and updatedAt, with staleness protection. By default, the highest priority, freshest instrument wins. You can override reconciliation with a custom reconcile function.
When the server says X but the client says Y, Symphony State provides reconciliation hooks:
- Optimistic updates: immediately update UI, then reconcile once server responds.
- Eventual consistency: keep local drafts until authoritative data catches up.
- Custom policies: write your own resolver to merge, prefer, or weight sources.
import type { ReconcileContext } from "@shiftbloom-studio/symphony-state";
const reconcile = <T extends { version: number }>(ctx: ReconcileContext<T>) => {
const entries = Object.entries(ctx.values).map(([id, value]) => ({ id, value }));
const winner = entries.sort((a, b) => b.value.version - a.value.version)[0];
return {
value: winner.value,
sourceId: winner.id,
updatedAt: ctx.meta[winner.id].updatedAt
};
};Note: This example assumes all values have a
versionproperty. In production code, add runtime checks or type guards to ensure type safety.
import { defineDerivedSection } from "@shiftbloom-studio/symphony-state";
const pricing = defineDerivedSection({
key: "pricing",
inputs: ["cart", "auth"],
compute: (cart, auth) => ({
total: cart.items.length * (auth.isPremium ? 0.8 : 1)
})
});Derived sections are read-only and recompute only when their inputs change.
conductor.transaction(() => {
conductor.getSection("auth").set({ userId: "42" });
conductor.getSection("cart").patch({ ownerId: "42" });
}, "login");All updates are staged, resolved in dependency order, and committed atomically.
import { createStorageSink } from "@shiftbloom-studio/symphony-state";
const auth = defineSection({
key: "auth",
source: createAtomAdapter({ userId: null }),
persist: createStorageSink({
key: "symphony-auth",
throttleMs: 200
})
});Use the built-in devtools panel or access orchestration snapshots programmatically. For a short setup guide covering transaction names and conditional rendering, see the DevTools recipe.
import { SymphonyDevTools } from "@shiftbloom-studio/symphony-state/devtools";
<SymphonyDevTools maxTransactions={10} />;// Keep a reference to your orchestrated adapter when wiring the section:
const postAdapter = createOrchestratedAdapter({
instruments: [
{ id: "server", source: serverCache, priority: 1, role: "server" },
{ id: "draft", source: localDraft, priority: 2, role: "optimistic" }
]
});
const posts = defineSection({
key: "posts",
source: postAdapter
});
// Later, inspect the current orchestration snapshot:
const snapshot = postAdapter.getSnapshot();getSnapshot() reports the active driver plus all instrument values, priorities, and staleness flags so you can see which source is in control.
// app/layout.tsx
"use client";
import { SymphonyProvider } from "@shiftbloom-studio/symphony-state/react";
import { conductor } from "./symphony";
export default function RootLayout({ children }) {
return <SymphonyProvider conductor={conductor}>{children}</SymphonyProvider>;
}// pages/_app.tsx
import { SymphonyProvider } from "@shiftbloom-studio/symphony-state/react";
import { conductor } from "../symphony";
export default function App({ Component, pageProps }) {
return (
<SymphonyProvider conductor={conductor}>
<Component {...pageProps} />
</SymphonyProvider>
);
}import { SymphonyScript } from "@shiftbloom-studio/symphony-state/react";
<SymphonyScript state={{ auth: { userId: "42" } }} />;| API | Description |
|---|---|
createConductor(config) |
Create a conductor instance. |
defineSection(def) |
Define a section backed by a source adapter. |
defineDerivedSection(def) |
Define a derived, read-only section. |
createOrchestratedAdapter(config) |
Orchestrate multiple sources with precedence. |
createAtomAdapter(initial) |
Built-in minimal store. |
createExternalStoreAdapter(store) |
Wrap an external get/set/subscribe store. |
createUrlParamsAdapter(options) |
Sync with URL search params. |
createStorageSink(options) |
Persist section values to storage. |
SymphonyProvider |
React context provider. |
useSection(key) |
React hook for section read/write. |
useSelector(key, selector) |
Selector hook with equality. |
createSymphony(config) |
Typed helper that wires a conductor and hooks. |
SymphonyDevTools |
Optional devtools panel. |
- Orchestration, not monolith: state sources remain independent.
- Deterministic propagation: dependency-ordered commit waves.
- Composable adapters: plug in external stores without boilerplate.
- SSR-safe: no unguarded
windowusage in core.
Apache-2.0