Skip to content

Shared state without the ceremony

A todo list shared across components: the list, a filter, derived counts, and actions to mutate it. Any component can read or change it.

In React, “shared state” means picking a pattern (Context + reducer? Zustand?) and building the plumbing before you write a single feature. In Reactra it’s one store block.


React — Context + useReducer (the no-extra-deps baseline)

Section titled “React — Context + useReducer (the no-extra-deps baseline)”
import { createContext, useContext, useReducer, useMemo, type ReactNode } from "react"
import { type Todo, type Filter, makeId } from "./todos"
type State = { items: Todo[]; filter: Filter }
type Action =
| { type: "add"; text: string }
| { type: "toggle"; id: string }
| { type: "selectFilter"; filter: Filter }
| { type: "clearCompleted" }
const reducer = (s: State, a: Action): State => {
switch (a.type) {
case "add": {
const t = a.text.trim()
return t ? { ...s, items: [...s.items, { id: makeId(), text: t, done: false }] } : s
}
case "toggle":
return { ...s, items: s.items.map((i) => (i.id === a.id ? { ...i, done: !i.done } : i)) }
case "selectFilter":
return { ...s, filter: a.filter }
case "clearCompleted":
return { ...s, items: s.items.filter((i) => !i.done) }
}
}
const TodoCtx = createContext<{ state: State; dispatch: React.Dispatch<Action> } | null>(null)
export function TodoProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(reducer, { items: [], filter: "all" })
return <TodoCtx.Provider value={{ state, dispatch }}>{children}</TodoCtx.Provider>
}
export function useTodos() {
const ctx = useContext(TodoCtx)
if (!ctx) throw new Error("useTodos must be used inside <TodoProvider>")
return ctx
}
// …and every consumer re-derives:
function Footer() {
const { state, dispatch } = useTodos()
const remaining = useMemo(() => state.items.filter((t) => !t.done).length, [state.items])
return (
<footer>
<span>{remaining} left</span>
<button onClick={() => dispatch({ type: "clearCompleted" })}>Clear completed</button>
</footer>
)
}

Plus: wrap the tree in <TodoProvider>, and every consumer does useTodos() + dispatch({ type: … }) + its own useMemo for derived values.

import { type Todo, type Filter, makeId } from "./todos"
session store todoStore {
state items: Todo[] = []
state filter: Filter = "all"
derived remaining = items.filter((t) => !t.done).length
derived visible = (
filter === "active" ? items.filter((t) => !t.done)
: filter === "completed" ? items.filter((t) => t.done)
: items
)
action add(text: string) {
const t = text.trim()
if (t) items = [...items, { id: makeId(), text: t, done: false }]
}
action toggle(id: string) { items = items.map((i) => i.id === id ? { ...i, done: !i.done } : i) }
action selectFilter(next: Filter) { filter = next }
action clearCompleted() { items = items.filter((t) => !t.done) }
}
// any consumer — no provider, no hook to write, no dispatch
export component Footer {
inject store todoStore { remaining, clearCompleted }
view {
<footer>
<span>{remaining} left</span>
<button onClick={clearCompleted}>Clear completed</button>
</footer>
}
}

  • The action-type union + the reducer + dispatch({ type }) → actions are methods that mutate: items = [...items, todo]
  • createContext + <TodoProvider> + the useTodos hookinject store todoStore { … } by name
  • Per-consumer useMemo for derivedderived lives in the store, computed once
  • Wrapping the tree in a providersession store declares its own lifetime

session is the scope: the list lives for the page session. Swap it for route store (per-route) or export store (app-wide singleton) — the keyword is the lifetime, no provider placement to reason about.


The store compiles to a closure-based factory registered in a small StoreRegistry; inject store becomes useReactraStore("todoStore") backed by useSyncExternalStore — React’s own external-store hook. No Context, no provider, no proxy.

Shared state is a declaration, not a plumbing project.