Reactive stores
Framework-agnostic reactive state primitives that power the React hooks
Kit ships two framework-agnostic reactive state containers - a reactive action store for on-demand async work and a reactive stream store for live notification streams. Both expose a tiny subscribe / getState contract that drops into any reactive system: React's useSyncExternalStore, Svelte stores, Vue's shallowRef, Solid's from(), or a hand-rolled render loop.
The @solana/react hooks are thin useSyncExternalStore wrappers over exactly these stores. If you are not using React - or you want imperative control the hooks don't expose - reach for the stores directly. Everything on this page is available from @solana/kit.
Action stores
A ReactiveActionStore wraps an async function as a reactive state machine. Each dispatch() runs the function and drives a { data, error, status } snapshot through idle → running → success (or error).
data and error persist through subsequent running states, so a view can keep rendering the last result while a retry is in flight (stale-while-revalidate). A success clears error, and a reset() clears data and error.
Any Kit RPC request is an action source: call .reactiveStore() on it to get a store that re-fires the same request on every dispatch().
The store API
| Member | What it does |
|---|---|
dispatch(...args) | Fire-and-forget. Returns synchronously and never throws - failures land on state as { status: 'error' }. Use from event handlers. |
dispatchAsync(...args) | Promise-returning. Resolves with the result, rejects with the thrown error (or an AbortError when superseded / reset). |
getState() | The current { data, error, status } snapshot. Stable identity between changes. |
subscribe(listener) | Registers a change listener; returns an unsubscribe function. The listener takes no argument - read the latest via getState(). |
reset() | Aborts any in-flight dispatch and returns the store to idle, clearing data and error. |
withSignal(signal) | A wrapper exposing dispatch / dispatchAsync bound to a caller-provided AbortSignal. |
Each dispatch() aborts the previous in-flight call, so only the most recent dispatch can mutate state - a stale response can never clobber a newer one.
Use withSignal to attach your own cancellation source. A fresh timeout per attempt:
Binding to your view
The subscribe / getState pair works with reactive UI frameworks. Wire the blockhash store to a "fetch" button:
Another common use case is to call dispatch on mount for data loading. React splits these into two hooks over the same store: useRequest fires on mount, useAction fires on demand.
Wrapping any async function
.reactiveStore() is sugar for RPC requests. For anything else - a fetch, your own SDK etc., you can build a store with createReactiveActionStore. The wrapped function receives the per-dispatch AbortSignal first, then whatever you pass to dispatch:
The signal is aborted automatically when a newer dispatch() supersedes this one or when reset() is called, so a function that threads it into its I/O cancels cleanly.
Stream stores
A ReactiveStreamStore holds the latest value from an ongoing stream. Where an action store fires on demand, a stream store opens a connection with connect() and updates its snapshot every time a notification arrives. Its status vocabulary is idle → loading → loaded (or error). data and error are preserved across loading, so a reconnect can render stale data while it re-establishes.
Any Kit RPC subscription is a stream source - call .reactiveStore() on it.
The store API
| Member | What it does |
|---|---|
connect() | Opens the stream (aborting any active connection) and transitions to loading, then loaded / error. |
getState() | The current { data, error, status } snapshot. Stable identity between changes. |
subscribe(listener) | Registers a change listener; returns an unsubscribe function. Read the latest via getState(). |
reset() | Aborts the active connection and returns the store to idle, clearing data and error. |
withSignal(signal) | A wrapper exposing connect() bound to a caller-provided AbortSignal - a per-connection timeout or a shared kill switch. |
Unlike an action store, a stream store is opened once and left running; reset() tears it down. A subsequent connect() always opens a fresh stream.
Binding to your view
The contract is the same subscribe / getState pair, but it is important to call reset() on teardown to clean up the stream. Wire the slot store to a live display:
Wrapping any stream
For a stream that is not a Kit subscription, you can build a store with createReactiveStoreFromDataPublisherFactory. You give it a factory that produces a fresh DataPublisher on every connect(), plus the channel names to read data and errors from. The factory receives the per-connection AbortSignal; thread it into the transport so the connection itself tears down on reset, not just the store's listeners.
The factory runs on every connect(), so a torn-down stream can be reopened without losing subscribers or the last known value.
Fetch once, then stay live
A common pattern is "load a value, then keep it current" - fetch an account balance once, then track it through subscription notifications. Doing this by hand is fiddly: the initial fetch and the first few notifications can arrive out of order, and a late initial response must not overwrite a newer notification.
createReactiveStoreWithInitialValueAndSlotTracking solves exactly this. It pairs an action source (the one-shot read) with a stream source (the live updates) and deduplicates the two by slot, so the store always holds the value observed at the highest slot. The result is an ordinary ReactiveStreamStore - connect() to start, and bind it with the same subscribe / getState pattern as any stream store. This is the primitive behind React's useTrackedData.
Both sources must yield SolanaRpcResponse envelopes so their slots can be compared; the two mappers project each source's value into the unified item type the store holds. The loaded data is itself a SolanaRpcResponse, so data.context.slot tells you the slot the current value was observed at.