Skip to content

Add react hooks for usePayer and useIdentity - #1876

Merged
mcintyre94 merged 1 commit into
mainfrom
payer-identity-hooks
Aug 5, 2026
Merged

Add react hooks for usePayer and useIdentity#1876
mcintyre94 merged 1 commit into
mainfrom
payer-identity-hooks

Conversation

@mcintyre94

@mcintyre94 mcintyre94 commented Jul 31, 2026

Copy link
Copy Markdown
Member

This PR adds usePayer and useIdentity react hooks, to surface the client.payer and client.identity values

These hooks are reactive, if the client implements the subscribeTo[Payer/Identity] then the hooks subscribe to it (using useSyncExternalStore) and always provide the latest value.

Unlike payer and identity which are not defined as optional on the client (the wallet plugin throws when it owns them and doesn't have a connected wallet to provide), I've made the hooks return TransactionSigner | undefined. This is because useSyncExternalStore doesn't really work with exceptions, it seems to tear down the subscription if the value it's trying to read throws, so it doesn't recover properly from a wallet simply being disconnected and reconnected etc. even with an ErrorBoundary in place. I think in react it makes more sense to just make these values optional.

@changeset-bot

changeset-bot Bot commented Jul 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 722cab6

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

This PR includes changesets to release 48 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-introspection 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

@bundlemon

bundlemon Bot commented Jul 31, 2026

Copy link
Copy Markdown

BundleMon

Files updated (3)
Status Path Size Limits
react/dist/index.browser.mjs
5.27KB (+153B +2.92%) -
react/dist/index.native.mjs
5.27KB (+153B +2.92%) -
react/dist/index.node.mjs
5.27KB (+153B +2.92%) -
Unchanged files (147)
Status Path Size Limits
@solana/kit production bundle
kit/dist/index.production.min.js
55.54KB -
errors/dist/index.node.mjs
21.6KB -
errors/dist/index.browser.mjs
21.58KB -
errors/dist/index.native.mjs
21.58KB -
rpc-graphql/dist/index.browser.mjs
18.82KB -
rpc-graphql/dist/index.native.mjs
18.82KB -
rpc-graphql/dist/index.node.mjs
18.82KB -
wallet-account-signer/dist/index.node.mjs
18.31KB -
wallet-account-signer/dist/index.browser.mjs
18.29KB -
wallet-account-signer/dist/index.native.mjs
18.29KB -
transaction-messages/dist/index.browser.mjs
11.34KB -
transaction-messages/dist/index.native.mjs
11.34KB -
transaction-messages/dist/index.node.mjs
11.34KB -
instruction-plans/dist/index.browser.mjs
7.02KB -
instruction-plans/dist/index.native.mjs
7.02KB -
instruction-plans/dist/index.node.mjs
7.02KB -
codecs-data-structures/dist/index.browser.mjs
5.29KB -
codecs-data-structures/dist/index.native.mjs
5.29KB -
codecs-data-structures/dist/index.node.mjs
5.29KB -
fixed-points/dist/index.browser.mjs
5.08KB -
fixed-points/dist/index.native.mjs
5.07KB -
fixed-points/dist/index.node.mjs
5.07KB -
offchain-messages/dist/index.browser.mjs
5.06KB -
offchain-messages/dist/index.native.mjs
5.06KB -
offchain-messages/dist/index.node.mjs
5.06KB -
kit/dist/index.browser.mjs
4.61KB -
kit/dist/index.native.mjs
4.6KB -
kit/dist/index.node.mjs
4.6KB -
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.15KB -
subscribable/dist/index.node.mjs
3.13KB -
keys/dist/index.node.mjs
3.06KB -
subscribable/dist/index.native.mjs
3.06KB -
subscribable/dist/index.browser.mjs
3.05KB -
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 -
transaction-introspection/dist/index.browser.
mjs
2.73KB -
transaction-introspection/dist/index.native.m
js
2.73KB -
transaction-introspection/dist/index.node.mjs
2.73KB -
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-subscriptions-spec/dist/index.node.mjs
2.23KB -
rpc-subscriptions-spec/dist/index.native.mjs
2.19KB -
rpc-subscriptions-spec/dist/index.browser.mjs
2.19KB -
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-types/dist/index.browser.mjs
1.9KB -
rpc-types/dist/index.native.mjs
1.9KB -
rpc-types/dist/index.node.mjs
1.9KB -
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 -
plugin-core/dist/index.browser.mjs
1.18KB -
plugin-core/dist/index.native.mjs
1.18KB -
plugin-core/dist/index.node.mjs
1.18KB -
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-spec-types/dist/index.browser.mjs
1.15KB -
rpc-spec-types/dist/index.native.mjs
1.15KB -
rpc-spec-types/dist/index.node.mjs
1.15KB -
rpc-api/dist/index.browser.mjs
1.04KB -
rpc-api/dist/index.native.mjs
1.04KB -
rpc-api/dist/index.node.mjs
1.04KB -
compat/dist/index.browser.mjs
969B -
compat/dist/index.native.mjs
968B -
compat/dist/index.node.mjs
966B -
rpc-spec/dist/index.browser.mjs
898B -
rpc-spec/dist/index.native.mjs
897B -
rpc-spec/dist/index.node.mjs
896B -
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 -
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 +459B +0.08%

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

@mcintyre94
mcintyre94 force-pushed the payer-identity-hooks branch from 8e5979d to a568f1f Compare July 31, 2026 17:21
@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 two new public hooks to @solana/react: usePayer(client) and useIdentity(client). Both read the corresponding signer off the client, and — when the client also advertises subscribeToPayer/subscribeToIdentity per the ClientWithSubscribeTo* convention — subscribe reactively via useSyncExternalStore so the returned value always reflects the latest signer. Clients without the subscribe hook fall back to a no-op subscribe and read the value once. To make the hooks robust against wallet-plugin getters that throw when no wallet is connected, both return TransactionSigner | undefined and swallow any error thrown by the getter into undefined — a deliberate departure from the non-optional plugin type. Ships with test-utility renderHook browser tests, __typetests__ for the public signature, and a minor changeset.

Also rolls in a batch of unrelated prettier reformatting across several existing test files and adds a blank line to examples/react-app/src/routes/root.tsx — presumably from pnpm style:fix. Harmless drive-by cleanup.

Key things to watch out for

  • Swallowing all errors from the getter is a design choice, not just wallet-not-connected. The try { return client.<x>; } catch { return undefined; } treats any exception — including bugs in a plugin's getter or a corrupted state — as "signer unavailable". The PR description justifies this via useSyncExternalStore's incompatibility with throwing snapshots. It's the right call for the wallet-disconnected path, but it does mean genuine bugs will silently surface as an absent signer rather than an error boundary hit. Might be worth a note in the docblock and/or a __DEV__ console.warn on caught errors so developers can spot unexpected throws during development.
  • Reactive contract requires a stable client.<x> reference for equal snapshots. useSyncExternalStore uses Object.is between snapshots; a getter that returns a new signer object on every read would cause infinite re-renders. That's the plugin's contract to uphold (and it's implied by subscribe-to.ts's "consumers that bail on reference-equal snapshots… will filter redundant notifications out for free"), but worth calling out for plugin authors.
  • README not updated. The @solana/react README's "Kit client bindings" section documents ClientProvider, useClient, useClientCapability, etc. and the repo's TypeScript READMEs skill says new public APIs should be added there. These two hooks should get a short section — especially because the undefined-on-throw behaviour and the "mutation without subscribeTo* isn't observed until the next render" semantic (which the last test in each file exercises) aren't obvious from the type signature alone.

Notes for subsequent reviewers

  • The hooks intentionally take client as an argument rather than reading it from ClientProvider via useClient/useClientCapability. That's consistent with the newer hook style (usePlanTransaction, useSendTransaction, etc.) but does differ from the useClient*-based pattern documented in the README's "Kit client bindings" intro — worth checking whether this is the intended direction for capability hooks going forward, and whether the README's framing should be adjusted.
  • The type tests use ClientWithSubscribeToIdentity/ClientWithSubscribeToPayer alone as the negative case (@ts-expect-error because the base capability is missing). That correctly verifies the base capability is required; there's no negative test for passing an object that has neither, which would also be worth locking in.
  • NOOP_UNSUBSCRIBE = () => {} is duplicated across both files. Fine as-is (one line, module-local), but if a third use<Capability> hook lands with the same pattern it would be worth pulling into a shared internal helper.
  • Changeset file has no trailing newline (\ No newline at end of file in the diff). Trivial — style:fix or the changeset CLI will normalize it.
  • Prettier reformatting in useAction-test.browser.tsx, useReactiveStoreLifecycle-test.browser.tsx, useSubscription-test.browser.tsx, useRequestQuery-test.browser.tsx, useRequestSWR-test.browser.tsx, and ClientProvider-test.browser.tsx is behaviour-neutral; safe to skim past.

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Documentation Preview: https://kit-docs-7y7nkuz0x-anza-tech.vercel.app

@mcintyre94
mcintyre94 force-pushed the payer-identity-hooks branch from a568f1f to 52df81f Compare August 3, 2026 10:59
@mcintyre94
mcintyre94 marked this pull request as ready for review August 3, 2026 11:00
@mcintyre94
mcintyre94 requested a review from lorisleiva August 3, 2026 11:01
@lorisleiva

Copy link
Copy Markdown
Member

@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.

Re-review

(Submitting as COMMENT since I don't have approve permissions on this repo — consider this an approval.)

Since my last pass, the main change is the new "Payer & identity" README section (packages/react/README.md), which addresses the primary point from my previous review. It documents both hooks, the undefined-while-unavailable return, and the reactive vs. fixed-client subscription behaviour — and the example is framed around useClient<AppClient>(), which also resolves my earlier note about aligning with the README's useClient-based intro. The changeset was also expanded to explicitly call out that a throwing plugin getter surfaces as undefined, which covers the discoverability concern about the swallow-all-errors behaviour at the release-notes level.

Everything else is unchanged from my prior review; I won't re-raise the minor points already noted there (NOOP_UNSUBSCRIBE duplication, missing trailing newline in the changeset, the optional __DEV__ warn on caught getter errors, and the absent negative typetest for a client with neither capability). All were explicitly optional and none block merging.

One truly optional thought: the docblocks and README say the hooks return undefined "while no payer/identity is available", which is accurate for the intended wallet-disconnected case but doesn't spell out that any exception from the getter is coerced to undefined. The changeset covers it, so I'm fine shipping as-is — just flagging in case you'd like the docblock @returns to mention it too.

Implementation, tests, and typetests all look solid. LGTM. 🚀

export function usePayer(client: ClientWithPayer & Partial<ClientWithSubscribeToPayer>): TransactionSigner | undefined {
const subscribe = useCallback(
(onStoreChange: () => void) =>
client.subscribeToPayer ? client.subscribeToPayer(onStoreChange) : NOOP_UNSUBSCRIBE,

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.

nit: Shouldn't NOOP_UNSUBSCRIBE be called NOOP_SUBSCRIBE? I'd be also happy to dissolve it for () => {} since it's pretty self-explanatory.

): TransactionSigner | undefined {
const subscribe = useCallback(
(onStoreChange: () => void) =>
client.subscribeToIdentity ? client.subscribeToIdentity(onStoreChange) : NOOP_UNSUBSCRIBE,

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.

Same here

mcintyre94 commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Merge activity

@mcintyre94
mcintyre94 force-pushed the example-app-system-plugin branch from 34797f3 to 8d77e0e Compare August 4, 2026 16:21
@mcintyre94
mcintyre94 force-pushed the payer-identity-hooks branch from 52df81f to 5308c32 Compare August 4, 2026 16:21
@mcintyre94
mcintyre94 changed the base branch from example-app-system-plugin to graphite-base/1876 August 5, 2026 17:09
@mcintyre94
mcintyre94 changed the base branch from graphite-base/1876 to main August 5, 2026 17:21
@mcintyre94
mcintyre94 force-pushed the payer-identity-hooks branch from 5308c32 to 722cab6 Compare August 5, 2026 17:52
@mcintyre94
mcintyre94 merged commit d6a1adb into main Aug 5, 2026
14 checks passed
@mcintyre94
mcintyre94 deleted the payer-identity-hooks branch August 5, 2026 18:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants