Skip to content

Add useSubscriptionSWR hook#1719

Open
mcintyre94 wants to merge 1 commit into
react/use-request-swrfrom
react/use-subscription-swr
Open

Add useSubscriptionSWR hook#1719
mcintyre94 wants to merge 1 commit into
react/use-request-swrfrom
react/use-subscription-swr

Conversation

@mcintyre94

@mcintyre94 mcintyre94 commented Jun 3, 2026

Copy link
Copy Markdown
Member

Summary of Changes

This PR adds useSubscriptionSWR, the SWR shaped version of useSubscription.

function AccountBalance({ address }: { address: Address }) {
    const client = useClient<ClientWithRpcSubscriptions<AccountNotificationsApi>>();
    const { data, error } = useSubscriptionSWR(
        address ? ['account', address] : null,
        address ? client.rpcSubscriptions.accountNotifications(address) : null,
    );
    if (error) return <p>Failed to connect.</p>;
    if (!data) return <p>Connecting…</p>;
    return (
        <p>
            {data.value.lamports} lamports at slot {data.context.slot}
        </p>
    );
}

Similarly to useRequestSWR the third param is an SWRConfiguration, with our usual getAbortSignal factory to add a custom signal. It also disables when either key or source are null. Note that SWR subscriptions don't include the reconnect function from useSubscription.

Note that under the hood this uses useSWRSubscription, which handles cleaning up the subscription and resetting the store.

@changeset-bot

changeset-bot Bot commented Jun 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b1c4037

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 47 packages
Name Type
@solana/react Major
@solana/accounts Major
@solana/addresses Major
@solana/assertions Major
@solana/codecs-core Major
@solana/codecs-data-structures Major
@solana/codecs-numbers Major
@solana/codecs-strings Major
@solana/codecs Major
@solana/compat Major
@solana/errors Major
@solana/fast-stable-stringify Major
@solana/fixed-points Major
@solana/functional Major
@solana/instruction-plans Major
@solana/instructions Major
@solana/keys Major
@solana/kit Major
@solana/nominal-types Major
@solana/offchain-messages Major
@solana/options Major
@solana/plugin-core Major
@solana/plugin-interfaces Major
@solana/program-client-core Major
@solana/programs Major
@solana/promises Major
@solana/rpc-api Major
@solana/rpc-graphql Major
@solana/rpc-parsed-types Major
@solana/rpc-spec-types Major
@solana/rpc-spec Major
@solana/rpc-subscriptions-api Major
@solana/rpc-subscriptions-channel-websocket Major
@solana/rpc-subscriptions-spec Major
@solana/rpc-subscriptions Major
@solana/rpc-transformers Major
@solana/rpc-transport-http Major
@solana/rpc-types Major
@solana/rpc Major
@solana/signers Major
@solana/subscribable Major
@solana/sysvars Major
@solana/transaction-confirmation Major
@solana/transaction-messages Major
@solana/transactions Major
@solana/wallet-account-signer Major
@solana/webcrypto-ed25519-polyfill Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

mcintyre94 commented Jun 3, 2026

Copy link
Copy Markdown
Member Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@bundlemon

bundlemon Bot commented Jun 3, 2026

Copy link
Copy Markdown

BundleMon

Files updated (25)
Status Path Size Limits
react/dist/index.browser.mjs
5.06KB (+1.96KB +63.36%) -
react/dist/index.native.mjs
5.05KB (+1.96KB +63.38%) -
react/dist/index.node.mjs
5.05KB (+1.96KB +63.4%) -
@solana/kit production bundle
kit/dist/index.production.min.js
53.03KB (+429B +0.8%) -
kit/dist/index.browser.mjs
4.81KB (+349B +7.62%) -
kit/dist/index.native.mjs
4.81KB (+349B +7.63%) -
kit/dist/index.node.mjs
4.81KB (+349B +7.63%) -
errors/dist/index.browser.mjs
20.9KB (+179B +0.84%) -
errors/dist/index.native.mjs
20.9KB (+179B +0.84%) -
errors/dist/index.node.mjs
20.92KB (+179B +0.84%) -
wallet-account-signer/dist/index.browser.mjs
17.69KB (+169B +0.94%) -
wallet-account-signer/dist/index.native.mjs
17.69KB (+168B +0.94%) -
wallet-account-signer/dist/index.node.mjs
17.71KB (+168B +0.93%) -
subscribable/dist/index.browser.mjs
2.76KB (+160B +6%) -
subscribable/dist/index.native.mjs
2.77KB (+160B +5.99%) -
subscribable/dist/index.node.mjs
2.83KB (+158B +5.76%) -
rpc-types/dist/index.browser.mjs
1.9KB (+102B +5.53%) -
rpc-types/dist/index.native.mjs
1.9KB (+101B +5.48%) -
rpc-types/dist/index.node.mjs
1.9KB (+101B +5.48%) -
rpc-subscriptions-spec/dist/index.browser.mjs
2.19KB (-13B -0.58%) -
rpc-subscriptions-spec/dist/index.native.mjs
2.19KB (-13B -0.58%) -
rpc-subscriptions-spec/dist/index.node.mjs
2.23KB (-13B -0.56%) -
rpc-spec/dist/index.browser.mjs
898B (-20B -2.18%) -
rpc-spec/dist/index.native.mjs
897B (-21B -2.29%) -
rpc-spec/dist/index.node.mjs
896B (-21B -2.29%) -
Unchanged files (122)
Status Path Size Limits
rpc-graphql/dist/index.browser.mjs
18.82KB -
rpc-graphql/dist/index.native.mjs
18.81KB -
rpc-graphql/dist/index.node.mjs
18.81KB -
transaction-messages/dist/index.browser.mjs
11.32KB -
transaction-messages/dist/index.native.mjs
11.32KB -
transaction-messages/dist/index.node.mjs
11.32KB -
instruction-plans/dist/index.browser.mjs
6.58KB -
instruction-plans/dist/index.native.mjs
6.58KB -
instruction-plans/dist/index.node.mjs
6.58KB -
fixed-points/dist/index.browser.mjs
5.08KB -
fixed-points/dist/index.native.mjs
5.07KB -
fixed-points/dist/index.node.mjs
5.07KB -
codecs-data-structures/dist/index.browser.mjs
5.04KB -
codecs-data-structures/dist/index.native.mjs
5.03KB -
codecs-data-structures/dist/index.node.mjs
5.03KB -
offchain-messages/dist/index.browser.mjs
4.89KB -
offchain-messages/dist/index.native.mjs
4.89KB -
offchain-messages/dist/index.node.mjs
4.89KB -
transactions/dist/index.browser.mjs
4.07KB -
transactions/dist/index.native.mjs
4.07KB -
transactions/dist/index.node.mjs
4.07KB -
codecs-core/dist/index.browser.mjs
3.62KB -
codecs-core/dist/index.native.mjs
3.62KB -
codecs-core/dist/index.node.mjs
3.62KB -
webcrypto-ed25519-polyfill/dist/index.node.mj
s
3.61KB -
webcrypto-ed25519-polyfill/dist/index.browser
.mjs
3.59KB -
webcrypto-ed25519-polyfill/dist/index.native.
mjs
3.57KB -
rpc-subscriptions/dist/index.browser.mjs
3.37KB -
rpc-subscriptions/dist/index.node.mjs
3.34KB -
rpc-subscriptions/dist/index.native.mjs
3.31KB -
signers/dist/index.browser.mjs
3.26KB -
signers/dist/index.native.mjs
3.26KB -
signers/dist/index.node.mjs
3.26KB -
rpc-transformers/dist/index.browser.mjs
3.16KB -
rpc-transformers/dist/index.native.mjs
3.16KB -
rpc-transformers/dist/index.node.mjs
3.16KB -
keys/dist/index.node.mjs
3.06KB -
addresses/dist/index.browser.mjs
2.93KB -
addresses/dist/index.native.mjs
2.92KB -
addresses/dist/index.node.mjs
2.92KB -
keys/dist/index.browser.mjs
2.85KB -
keys/dist/index.native.mjs
2.85KB -
codecs-strings/dist/index.browser.mjs
2.55KB -
codecs-strings/dist/index.node.mjs
2.51KB -
codecs-strings/dist/index.native.mjs
2.47KB -
transaction-confirmation/dist/index.node.mjs
2.42KB -
transaction-confirmation/dist/index.native.mj
s
2.37KB -
sysvars/dist/index.browser.mjs
2.37KB -
sysvars/dist/index.native.mjs
2.37KB -
transaction-confirmation/dist/index.browser.m
js
2.37KB -
sysvars/dist/index.node.mjs
2.37KB -
rpc/dist/index.node.mjs
1.95KB -
codecs-numbers/dist/index.browser.mjs
1.95KB -
codecs-numbers/dist/index.native.mjs
1.95KB -
codecs-numbers/dist/index.node.mjs
1.94KB -
rpc-transport-http/dist/index.browser.mjs
1.89KB -
rpc-transport-http/dist/index.native.mjs
1.89KB -
rpc/dist/index.native.mjs
1.81KB -
rpc/dist/index.browser.mjs
1.8KB -
rpc-transport-http/dist/index.node.mjs
1.71KB -
rpc-subscriptions-channel-websocket/dist/inde
x.node.mjs
1.33KB -
rpc-subscriptions-channel-websocket/dist/inde
x.native.mjs
1.27KB -
rpc-subscriptions-channel-websocket/dist/inde
x.browser.mjs
1.26KB -
program-client-core/dist/index.browser.mjs
1.21KB -
program-client-core/dist/index.native.mjs
1.21KB -
program-client-core/dist/index.node.mjs
1.21KB -
options/dist/index.browser.mjs
1.18KB -
options/dist/index.native.mjs
1.18KB -
options/dist/index.node.mjs
1.17KB -
accounts/dist/index.browser.mjs
1.17KB -
accounts/dist/index.native.mjs
1.17KB -
accounts/dist/index.node.mjs
1.16KB -
rpc-api/dist/index.browser.mjs
998B -
rpc-api/dist/index.native.mjs
997B -
rpc-api/dist/index.node.mjs
995B -
compat/dist/index.browser.mjs
969B -
compat/dist/index.native.mjs
968B -
compat/dist/index.node.mjs
966B -
rpc-spec-types/dist/index.browser.mjs
962B -
rpc-spec-types/dist/index.native.mjs
961B -
rpc-spec-types/dist/index.node.mjs
959B -
rpc-subscriptions-api/dist/index.native.mjs
871B -
rpc-subscriptions-api/dist/index.browser.mjs
870B -
rpc-subscriptions-api/dist/index.node.mjs
870B -
promises/dist/index.native.mjs
841B -
promises/dist/index.node.mjs
840B -
promises/dist/index.browser.mjs
839B -
plugin-core/dist/index.browser.mjs
820B -
plugin-core/dist/index.native.mjs
819B -
plugin-core/dist/index.node.mjs
817B -
assertions/dist/index.browser.mjs
783B -
instructions/dist/index.browser.mjs
771B -
instructions/dist/index.native.mjs
770B -
instructions/dist/index.node.mjs
768B -
fast-stable-stringify/dist/index.browser.mjs
726B -
fast-stable-stringify/dist/index.native.mjs
725B -
assertions/dist/index.native.mjs
724B -
fast-stable-stringify/dist/index.node.mjs
724B -
assertions/dist/index.node.mjs
723B -
programs/dist/index.browser.mjs
329B -
programs/dist/index.native.mjs
327B -
programs/dist/index.node.mjs
325B -
fs-impl/dist/index.browser.mjs
245B -
event-target-impl/dist/index.node.mjs
230B -
functional/dist/index.browser.mjs
154B -
functional/dist/index.native.mjs
152B -
text-encoding-impl/dist/index.native.mjs
152B -
functional/dist/index.node.mjs
151B -
codecs/dist/index.browser.mjs
145B -
codecs/dist/index.native.mjs
144B -
codecs/dist/index.node.mjs
142B -
event-target-impl/dist/index.browser.mjs
133B -
ws-impl/dist/index.node.mjs
131B -
text-encoding-impl/dist/index.browser.mjs
122B -
fs-impl/dist/index.node.mjs
120B -
text-encoding-impl/dist/index.node.mjs
119B -
ws-impl/dist/index.browser.mjs
113B -
crypto-impl/dist/index.node.mjs
111B -
crypto-impl/dist/index.browser.mjs
109B -
rpc-parsed-types/dist/index.browser.mjs
66B -
rpc-parsed-types/dist/index.native.mjs
65B -
rpc-parsed-types/dist/index.node.mjs
63B -

Total files change +9.01KB +1.71%

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Documentation Preview: https://kit-docs-945nn6vhe-anza-tech.vercel.app

@mcintyre94

Copy link
Copy Markdown
Member Author

@trevor-cortex

@trevor-cortex trevor-cortex left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Adds useSubscriptionSWR — the SWR-backed counterpart to useSubscription, mirroring the shape and conventions of useRequestSWR. It wraps useSWRSubscription, accepts a ReactiveStreamSource<T> and SWR key (either nullable to disable), splits off the Kit-specific getAbortSignal factory from the rest of the SWR config, ref-syncs source/factory so inline values don't churn the subscribe callback's identity, and tears down the underlying store on cleanup via unsubscribe() + store.reset(). Multiple components with the same key share one underlying subscription (SWR dedupes), and either-null gating works by forcing the SWR key to null when source is null. Ships with a thorough browser test suite (envelope vs raw notifications, error channel, both-null gating, transitions in/out of null, key changes, dedup, abort-signal forwarding, SSR), a typetest covering envelope/raw shapes, default-unknown error type and overrides, options forwarding, and a minor changeset.

Things to watch out for

  • No reconnect() affordance. useSubscription exposes a reconnect() callback as part of its result, but SWRSubscriptionResponse doesn't have an equivalent and there's no way to force useSWRSubscription to re-open a stream while the key is stable (SWR's mutate doesn't reopen subscriptions). In practice the only way to reconnect is to change the SWR key. This is an inherent limitation of useSWRSubscription, not a bug, but it's a meaningful semantic difference from useSubscription that the README doesn't currently call out — worth a sentence so users picking between the two hooks aren't surprised.
  • "Per-connection signals" wording in the README is slightly off. Because there's no reconnect(), getAbortSignal is invoked once per subscribe invocation (i.e. once per unique key, deduped across components), not once per connection in the useSubscription sense. Minor doc nit.
  • Bump level. minor for a new public hook is correct per the repo's CLAUDE.md (pre-1.0, new feature).

Notes for subsequent reviewers

  • The subscribe callback subscribes to the store before calling connect(), which is the right order to avoid missing synchronous state transitions. The listener only forwards loaded/error states to next; loading/idle are correctly ignored so SWR's data retains its prior value across reconnect-style transitions (stale-while-revalidate is preserved at the kit-store layer, even though useSWRSubscription itself doesn't have that concept).
  • The shared-subscription test ('shares one active underlying subscription across components with the same key') relies on SWR's per-key dedup: only one subscribe invocation fires per key, so sourceRef.current from whichever render wins is what gets used. Worth being aware of if two callers ever pass different sources under the same key — the later mount won't re-subscribe.
  • The useCallback(subscribe, []) + ref-sync pattern matches useRequestSWR exactly. Good consistency.
  • Cleanup calls both unsubscribe() (removes our listener) and store.reset() (aborts the connection). Since the store is created locally per subscribe invocation and has no other listeners, this is the correct teardown.
  • The abort-signal test is genuinely exercising the kit store's abort handling (not the fake publisher's listener removal) — the store transitions to error with the abort reason, which the subscribe listener then forwards via next(reason). Good coverage.

Comment thread packages/react/src/swr/useSubscriptionSWR.ts Outdated
Comment thread packages/react/README.md Outdated
Comment on lines +49 to +53
const getAbortSignalRef = useRef(getAbortSignal);
useIsomorphicLayoutEffect(() => {
sourceRef.current = source;
getAbortSignalRef.current = getAbortSignal;
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: consider seeding from store.getUnifiedState() immediately after subscribing, in case the store is ever in a non-idle state at the moment we attach (e.g. a future change where stores can be pre-warmed via a cache). Today every source.reactiveStore() returns an idle store so this is a no-op, but it would make the bridge resilient to that assumption changing. Not blocking.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with this, and I think this is quite important because a store defined by a plugin can have whatever behaviour that plugin wants. This makes the hook a bit more complex, but is worth it IMO.

Note that other hooks don't have this issue because useSyncExternalStore handles the initial state.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually this is wrong and I've reverted the change. .reactiveStore() is documented as returning an idle store, and this is an important part of the design that we settled on. So useSubscriptionSWR can trust the state to start idle and only change after it has subscribed and connected.

@mcintyre94 mcintyre94 force-pushed the react/use-request-swr branch from 773481f to 5d60581 Compare June 3, 2026 15:17
@mcintyre94 mcintyre94 force-pushed the react/use-subscription-swr branch 2 times, most recently from 3b9a423 to f3f39e1 Compare June 3, 2026 15:51
@mcintyre94 mcintyre94 marked this pull request as ready for review June 3, 2026 17:01
@mcintyre94 mcintyre94 requested a review from lorisleiva June 3, 2026 17:01
@mcintyre94 mcintyre94 marked this pull request as draft June 5, 2026 14:24
@mcintyre94 mcintyre94 force-pushed the react/use-subscription-swr branch from f3f39e1 to 7f08fc4 Compare June 5, 2026 15:13
@mcintyre94 mcintyre94 marked this pull request as ready for review June 5, 2026 15:18
@mcintyre94 mcintyre94 force-pushed the react/use-request-swr branch from 5d60581 to d78c67a Compare June 5, 2026 15:39
@mcintyre94 mcintyre94 force-pushed the react/use-subscription-swr branch from 7f08fc4 to 948efe9 Compare June 5, 2026 15:39
@mcintyre94 mcintyre94 force-pushed the react/use-request-swr branch from d78c67a to b30ff51 Compare June 5, 2026 15:47
@mcintyre94 mcintyre94 force-pushed the react/use-subscription-swr branch 2 times, most recently from 603b8f5 to 783f65e Compare June 5, 2026 15:56
@mcintyre94 mcintyre94 changed the title Add useSubscriptionSWR adapter Add useSubscriptionSWR hook Jun 5, 2026
@mcintyre94 mcintyre94 force-pushed the react/use-subscription-swr branch from 783f65e to f835130 Compare June 5, 2026 17:48
@mcintyre94 mcintyre94 force-pushed the react/use-request-swr branch from 1e1ddab to 16debfb Compare June 5, 2026 17:48
@mcintyre94 mcintyre94 force-pushed the react/use-subscription-swr branch from f835130 to 378f178 Compare June 5, 2026 17:56

@lorisleiva lorisleiva left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@mcintyre94 mcintyre94 force-pushed the react/use-subscription-swr branch from 378f178 to 7491dbe Compare June 10, 2026 15:43
@mcintyre94 mcintyre94 force-pushed the react/use-request-swr branch from 16debfb to 1dc93f1 Compare June 10, 2026 15:43
@mcintyre94 mcintyre94 force-pushed the react/use-subscription-swr branch from 7491dbe to de50afe Compare June 10, 2026 16:49
@mcintyre94 mcintyre94 force-pushed the react/use-request-swr branch from 1dc93f1 to 6a213e8 Compare June 10, 2026 16:49
@mcintyre94 mcintyre94 added the do-not-close Add this tag to exempt an issue/PR from being closed by the stalebot label Jun 11, 2026
@mcintyre94 mcintyre94 force-pushed the react/use-subscription-swr branch from de50afe to d07c735 Compare June 12, 2026 17:53
Add an SWR-backed subscription hook for ReactiveStreamSource values on the @solana/react/swr subpath. The hook shares subscriptions by SWR key, returns SWR's native data/error shape, supports nullable key or source gating, and threads Kit's getAbortSignal option into each connection.

Document the new hook, export it from the SWR entrypoint, and cover notification passthrough, error handling, disabled states, key/source lifecycle behavior, shared subscriptions, StrictMode rendering, SSR safety, and public typing.
@mcintyre94 mcintyre94 force-pushed the react/use-subscription-swr branch from d07c735 to b1c4037 Compare June 12, 2026 18:28
@mcintyre94 mcintyre94 force-pushed the react/use-request-swr branch from 51dc371 to 34a17a4 Compare June 12, 2026 18:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-close Add this tag to exempt an issue/PR from being closed by the stalebot

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants