Store scopes & disposal

Make ownership visible: fresh state for each independent scope, and one explicit cleanup path when that scope ends.

A factory is the isolation boundary

For independent chart panels, editors, or SSR requests, create slices inside the store factory and allocate every nested initial-state object there. Reusing one slice can reuse its initial-state objects. A detached DevTools snapshot does not isolate live state.

preloadedState replaces the root initial state; it is not merged with it. Supply a complete state shape and allocate or copy it per scope, including nested arrays and objects. Avoid module-level singleton stores in SSR handlers.

chart.ts · the running example
import { configureStore, createEvent, createSlice } from "@carere/solux";

export const appendPoint = createEvent<number>("chart/pointAdded");

// Each call creates a fresh slice AND fresh nested state.
// Share event creators, never a slice's initial-state object.
export function createChartStore() {
  const rootSlice = createSlice({
    initialState: { series: { points: [24, 38, 30, 54] } },
    handlers: (builder) =>
      builder.addHandler(appendPoint, (state, { payload }) => {
        state.series.points.push(payload);
        if (state.series.points.length > 12) state.series.points.shift();
      }),
  });
  return configureStore({ rootSlice });
}
Two stores, one event type

Synthetic data. Add a point in A; B keeps its own state. Dispose A, try a dispatch, then recreate it with fresh state.

Chart A

Active · #1
Points
4
Notifications
0
{"series":{"points":[24,38,30,54]}}

No events yet

Chart B

Active · #1
Points
4
Notifications
0
{"series":{"points":[24,38,30,54]}}

No events yet

Disposal closes the core event stream. Final state stays readable. Navigating away explicitly disposes both stores.

Dispose what you create

dispose() closes store subscriptions and registered enhancer resources once. Later core dispatches do nothing; new core subscriptions are closed without replaying the last event. The final state remains readable. You can unsubscribe an individual listener without disposing the store.

The public store exposes state, dispatch, subscribe, subscribeToEvent, and dispose. onDispose, initialState, and setState are enhancer-only capabilities and are stripped from the returned public store. Custom enhancers should preserve lifecycle methods when returning a store.

The provider and Solid owner do not own store cleanup automatically. Register store.dispose in onCleanup yourself when the component owns the store. Recreating a scope means creating a new store, not reviving a disposed one.

Component-owned chart scope
import { onCleanup } from "solid-js";
import { createChartStore, appendPoint } from "./chart";

function ChartPanel() {
  const store = createChartStore();
  const subscription = store.subscribe(({ event }) => console.log(event));
  // Keep a real chart handle outside serializable state.
  onCleanup(() => {
    subscription.unsubscribe();
    store.dispose();
    // Dispose the chart instance / abort caller-owned work here too.
  });
  return <button onClick={() => store.dispatch(appendPoint(42))}>
    Points: {store.state.series.points.length}
  </button>;
}

Give each request its own store

Use a request-scoped factory and a finally block around the render operation. For streaming SSR, keep the store alive until the stream is finished or aborted rather than disposing as soon as you create the stream. If you hydrate preloaded state in the browser, transfer only serializable data and escape it safely using your framework’s serialization facilities.

Non-streaming request ownership · pseudocode integration
async function handleRequest() {
  const store = createChartStore();
  try {
    return await renderRequestWithStore(store);
  } finally {
    store.dispose(); // Unreleased API; no automatic epic cancellation.
  }
}