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().

import {  } from '@solana/kit';
 
const  = ('https://api.devnet.solana.com');
 
// The store starts `idle`. The arguments are baked into the request, so each
// dispatch() re-fires the same getLatestBlockhash call.
const  = .().();
 
const  = .(() => {
    const { , ,  } = .();
    if ( === 'success') {
        .('Blockhash:', ..);
    } else if ( === 'error') {
        .();
    }
});
 
.(); // fire the first attempt

The store API

MemberWhat 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:

import {  } from '@solana/kit';
 
const  = ('https://api.devnet.solana.com');
const  = .().();
 
// Fresh 5-second clock on every attempt:
.(.(5_000)).();

Binding to your view

The subscribe / getState pair works with reactive UI frameworks. Wire the blockhash store to a "fetch" button:

import {  } from '@solana/kit';
 
const  = ('https://api.devnet.solana.com');
const  = .().();
 
const  = .('button')!;
const  = .('#out')!;
 
.('click', () => .());
 
const  = .(() => {
    const  = .();
    . = . === 'success' ? ... : .;
});
// On teardown: unsubscribe();

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:

import {  } from '@solana/kit';
 
const  = (async (: AbortSignal, : string) => {
    const  = await (`/api/accounts/${}`, {  });
    return .();
});
 
.(() => .(.()));
.('abc...'); // forwarded to the wrapped function after the signal

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.

import {  } from '@solana/kit';
 
const  = ('wss://api.devnet.solana.com');
 
const  = .().();
 
const  = .(() => {
    const { ,  } = .();
    if ( === 'loaded') {
        .('Current slot:', .);
    }
});
 
.(); // open the stream

The store API

MemberWhat 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:

import {  } from '@solana/kit';
 
const  = ('wss://api.devnet.solana.com');
const  = .().();
 
const  = .('#slot')!;
const  = .(() => {
    const  = .();
    . = . === 'loaded' ? (..) : .;
});
 
.();
// On teardown: unsubscribe(); store.reset();

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.

import {  } from '@solana/kit';
 
const  = <string>({
    : () => {
        const  = new ('wss://example.com/feed');
        .('abort', () => .());
        // A `DataPublisher` exposes `on(channel, subscriber, { signal })`. Forward the
        // payload you care about from each channel - here, the message text.
        return .({
            (, , ) {
                if ( === 'message') {
                    const  = (: ) => (.);
                    .('message', , );
                    return () => .('message', );
                }
                // channelName === 'error' here — surface a real Error rather than the raw DOM event.
                const  = () => (new ('WebSocket connection error'));
                .('error', , );
                return () => .('error', );
            },
        });
    },
    : 'message',
    : 'error',
});
 
.(() => {
    const  = .();
    if (. === 'loaded') .('Latest:', .);
});
 
// Fresh 30-second clock per connection attempt:
.(.(30_000)).();

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.

import {
    ,
    ,
    ,
    ,
} from '@solana/kit';
 
const  = ('https://api.devnet.solana.com');
const  = ('wss://api.devnet.solana.com');
const  = ('FnHyam9w4NZoWR6mKN1CuGBritdsEWZQa4Z4oawLZGxa');
 
const  = ({
    : .(, { : 'confirmed' }),
    : () => ,
    : .(),
    : ({  }) => ,
});
 
const  = .(() => {
    const  = .();
    if (. === 'loaded') {
        .(`Balance at slot ${...}:`, ..);
    }
});
 
.();

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.

On this page