stellar/laboratory · Archived

zustand-store-patterns

Reference for the two Zustand store patterns in Stellar Lab — main store (querystring) and transaction flow store (sessionStorage). Invoke when creating store slices, debugging hydration, or deciding where state belongs.

First seen Jun 22, 2026

Installation

$ npx skills add stellar/laboratory --skill zustand-store-patterns

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from stellar/laboratory.

npx skills add stellar/laboratory

Browse all from stellar/laboratory

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 109
License LICENSE
Default branch main
Open issues 42
Status Archived

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 5,489 B
  • docs SUMMARY.md 252 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 1 installs

SKILL.md

Zustand Store Patterns

Two Stores, Two Purposes

Main Store Transaction Flow Store
File src/store/createStore.ts src/store/createTransactionFlowStore.ts
Hook useStore() useBuildFlowStore()
Middleware querystring(immer(...)) persist(immer(...))
Persists to URL querystring sessionStorage
Survives tab close Yes (URL) No
Survives refresh Yes (URL) Yes (sessionStorage)
Instance type New per provider mount Singleton (module-level)
Used by Entire app (~85 files) Transaction build flow (~21 files)

When to Use Which

Main store (useStore): For state that is app-wide, URL-shareable, or used outside the transaction flow. Network settings, XDR viewer state, endpoint explorer state, account page state.

Flow store (useBuildFlowStore): For state within the single-page transaction flow. Build params, operations, simulation results, signed XDR, step navigation. This state is too large for URLs (simulation results can be 50KB+).

Cross-store reads: Components in the transaction flow read network from the main store as a read-only dependency. Never duplicate network config into the flow store.

export const SimulateStepContent = () => {
  const { network } = useStore();              // Main store — network only
  const { build, simulate } = useBuildFlowStore(); // Flow store — tx state
};

Main Store Pattern

Middleware stack

create<Store>()(
  querystring(
    immer((set) => ({ ... })),
    {
      url: options.url,
      select() { /* what to persist in URL */ },
    }
  )
);

Hydration

Synchronous — URL is passed at creation time in StoreProvider:

// src/store/StoreProvider.tsx
const [store] = useState(() => createStore({ url }));

No manual rehydration needed. Store re-creates on route change.

Consumption

Via React Context (because instance is dynamic):

import { useStore } from "@/store/useStore";

const { network } = useStore();
const network = useStore((s) => s.network); // selector form

Transaction Flow Store Pattern

Middleware stack

create<TransactionFlowStore>()(
  persist(
    immer((set) => ({ ... })),
    {
      name: "stellar_lab_tx_flow_build",
      storage: createJSONStorage(() => sessionStorage),
      skipHydration: true,  // CRITICAL
    }
  )
);

Hydration — The Critical Gotcha

Never rehydrate in useEffect. Child effects fire before parent effects and will write default state to sessionStorage, overwriting persisted data.

// WRONG — don't do this
useEffect(() => {
  useBuildFlowStore.persist.rehydrate();
}, []);

// CORRECT — module-scope rehydration (runs before any component mounts)
if (typeof window !== "undefined") {
  useBuildFlowStore.persist.rehydrate();
}

This runs at module load time, before React renders anything. The skipHydration: true flag prevents Zustand's automatic rehydration (which happens too late).

Consumption

Direct import (singleton, no context needed):

import { useBuildFlowStore } from "@/store/createTransactionFlowStore";

const { build, simulate, setActiveStep } = useBuildFlowStore();

Storage keys

  • stellarlabtxflowbuild — Build flow state
  • stellarlabtxflowimport — Import flow state (independent)

Each flow has its own store instance and sessionStorage key. Navigating between Build and Import tabs does not clear the other's state.

Adding New State

To the main store

  1. Add the field + action to createStore.ts
  2. If it should persist in URL, add to the select() config
  3. Consume via useStore()

To the flow store

  1. Add the field to the appropriate slice (build, simulate, sign,

validate, submit)

  1. Add setter action(s) using Immer's mutable syntax:

``typescript setSignedXdr: (xdr: string) => set((state) => { state.sign.signedXdr = xdr; }), ``

  1. Add to INITIAL_STATE with a sensible default
  2. Add to reset logic if the field should clear when the user goes back to Build
  3. Consume via useBuildFlowStore()

Data conventions

  • XDR values: Always store as base64 strings, never SDK objects
  • RPC results: Store as raw JSON strings (JSON.stringify(result))
  • Step navigation: activeStep and highestCompletedStep are persisted —

they survive refresh

  • Validation state: Can only raise highestCompletedStep, never lower it

(except on explicit reset)

Error Handling for sessionStorage

All sessionStorage access must be wrapped in try-catch:

  • Write failure (QuotaExceededError): Fall back to in-memory only
  • Read failure (corrupted JSON): Reset to default state
  • Private mode: sessionStorage works but clears on tab close (same behavior)

Key Files

Purpose File
Main store src/store/createStore.ts
Main store hook src/store/useStore.ts
Store provider src/store/StoreProvider.tsx
Flow store src/store/createTransactionFlowStore.ts
Root layout src/app/layout.tsx