Guides: Sharing state

Guides

Sharing state

State declared with $state lives where it's declared. Declared inside a component, each instance gets its own. Declared at the top of a module, it's one value shared by everything that module renders. To share state between files, put it in a module of its own and export it.

A store module

Create src/lib/cart.ts:

src/lib/cart.ts
export interface Item {
	id: number;
	name: string;
}

export let cart = $store({ items: [] as Item[] });

export function add(item: Item) {
	cart.items.push(item);
}

export function clear() {
	cart.items = [];
}

$store is $state meant for exporting: it compiles the same way, but the compiler doesn't warn that it's unused in its own file. Any file can import it:

src/lib/CartBadge.tsx
import { cart } from "./cart";

export const CartBadge = () => (
	<span class="badge">{cart.items.length}</span>
);
src/routes/shop.tsx
import { add } from "../lib/cart";
import { CartBadge } from "../lib/CartBadge";

const Shop = () => (
	<>
		<CartBadge />
		<button onClick={() => add({ id: Date.now(), name: "Tea" })}>Add tea</button>
	</>
);

export default Shop;

Clicking the button updates the badge. Reads of an imported store are tracked like reads of local state, so any expression that uses cart.items re-runs when the list changes.

Change it through functions

An import binding is read-only. cart.items.push(…) works from any file, because it changes the object behind the binding, but assigning the binding itself (cart = …) only works in the module that declares it. Export functions for changes, as add and clear do above. That also keeps every way the state can change in one place.

The same applies to a store holding a single value:

src/lib/theme.ts
export let theme = $store<"light" | "dark">("light");

export function toggleTheme() {
	theme = theme === "light" ? "dark" : "light";
}

Importers read theme as a plain value, and {theme} in JSX updates when toggleTheme() runs.

Derived values

$derived works across files too. It re-runs when the store values it read change:

src/lib/CartTotal.tsx
import { cart } from "./cart";

export const CartTotal = () => {
	let count = $derived(cart.items.length);
	let label = $derived(count === 1 ? "1 item" : `${count} items`);
	return <p>{label}</p>;
};

Stores on the server

In the browser a store belongs to one visitor, which is what you want. On the server the same module serves everyone. Two rules keep that safe:

  • Fill per-user stores in the browser, from data a load function returned, not while rendering on the server.
  • For per-request values the server needs while rendering, use context instead.

Context

setContext and getContext from @sigil-dev/runtime hand a value to components further down without passing it through every prop. On the server each request gets its own set of context values; in the browser there's one set for the page.

src/lib/user.ts
import { createContext } from "@sigil-dev/runtime";

export interface User {
	name: string;
}

export const UserKey = createContext<User | null>();
src/lib/Greeting.tsx
import { getContext } from "@sigil-dev/runtime";
import { UserKey } from "./user";

export const Greeting = () => {
	const user = getContext(UserKey);
	return <p>{user ? `Hello, ${user.name}` : "Hello"}</p>;
};
src/routes/account/+page.tsx
import { setContext } from "@sigil-dev/runtime";
import { Greeting } from "../../lib/Greeting";
import { type User, UserKey } from "../../lib/user";

const Account = ({ data }: { data: { user: User | null } }) => {
	setContext(UserKey, data.user);
	return (
		<main>
			<Greeting />
		</main>
	);
};

export default Account;

Three things to know:

  • A component sees context set before it renders. A page renders before the layouts around it, so context set in a layout doesn't reach the page. For data a layout loads, have the page's load read it with parent() (Load functions).
  • Context isn't scoped to part of the tree: there's one value per key, and a second setContext with the same key replaces the first.
  • It isn't reactive. For values that change, put a store in context, or pass props.

Edit this page