Jotai State Management in the Agent Playground#
Overview#
The agent playground uses a custom Jotai atomStore paired with a SWR middleware stack for state management. The two systems serve different roles:
- Jotai
atomStore(createStore()) β a singleton module-level store used as a long-lived revision cache. It holds all fetchedEnhancedVariantrevisions and other cross-cutting state (metadata, responses, active fetches) outside of the React tree. - SWR β drives the primary data-fetching and UI-sync lifecycle. Each consumer calls
usePlayground(), which composes six middleware layers on top ofuseSWR.
The atomStore is defined in web/oss/src/lib/hooks/useStatelessVariants/state/index.tsx. It is not provided via React context β it's a plain module export accessed directly anywhere in the codebase.
atomStore Atoms#
| Atom | Purpose |
|---|---|
allRevisionsAtom | All fetched EnhancedVariant revisions (full registry) |
variantsRefAtom | Keyed map of EnhancedVariant by hash |
metadataAtom | ConfigMetadata keyed by hash |
responseAtom | TestResult keyed by hash |
specAtom | OpenAPI spec |
activeFetchesAtom | Active AbortController per fetch key |
All atoms are read/written via the bare atomStore.get() / atomStore.set() API (not React hooks).
SWR Middleware Stack#
usePlayground() composes the following middlewares in order :
playgroundUIMiddlewareβ managesdisplayedVariantsandviewTypeplaygroundVariantsMiddlewareβ handles multi-variant orchestration and test runsplaygroundVariantMiddlewareβ per-variant param updates and save operationsappSchemaMiddlewareβ fetches revisions, initializes SWR state, triggers background loadisVariantDirtyMiddlewareβ tracks unsaved changes per variantselectorMiddlewareβ scoped data access viavariantSelector/stateSelector
The key for the SWR cache is derived from appId, projectId, and the route path .
Anti-Patterns and the "Detected store mutation during atom read" Warning#
Jotai emits Detected store mutation during atom read when an atom's state changes while Jotai is in the middle of computing atom values. Three patterns in the playground trigger this:
1. Side-effecting property getters#
Every middleware returns the SWR result with properties exposed as Object.defineProperty getters . When a getter is accessed during Jotai's atom-read phase (e.g., inside a derived atom or an atom initializer), any side effect it triggers β including calling addToValueReferences or atomStore.set() β is treated as a mutation during read.
For example, playgroundUIMiddleware defines displayedVariants, viewType, and action setters all as getters on the SWR object . Similarly, selectorMiddleware defines a propertyGetter accessor .
2. Microtask-scheduled mutations#
appSchemaMiddleware uses queueMicrotask() to defer background revision loading without blocking the initial render . Inside that microtask, it calls atomStore.set(allRevisionsAtom, ...) and then triggers a SWR mutate(). If Jotai has started an atom read in the same microtask checkpoint, the atomStore.set lands during the read phase, producing the warning.
3. Direct store access outside React hooks#
Utility functions like getResponseLazy, getMetadataLazy, getAllVariants, and startFetch/endFetch read from and write to atomStore imperatively β bypassing React's render cycle entirely. This is intentional for performance (lazy reads on demand, no hook subscriptions), but it means Jotai cannot track dependency graphs for these accesses. If any caller invokes a setter during a read, the warning fires.
Note from PR #5323: the warning is a known anti-pattern, not a crash cause. It was explicitly separated from the refractor chunk failure that caused playground remounts and was flagged for a dedicated follow-up .
Key Source Files#
| File | Role |
|---|---|
| state/index.tsx | atomStore, all atom definitions, lazy readers/writers |
| usePlayground/index.ts | Middleware composition, SWR hook entry point |
| appSchemaMiddleware.ts | Fetcher, background revision loading via queueMicrotask |
| playgroundUIMiddleware.ts | UI state getters, atomStore.get(allRevisionsAtom) for variant resolution |
| selectorMiddleware.ts | variantSelector/stateSelector scoped access pattern |