Skip to content

Add ClientProvider, useClient, and useClientCapability#1607

Open
mcintyre94 wants to merge 1 commit intomainfrom
react/provider
Open

Add ClientProvider, useClient, and useClientCapability#1607
mcintyre94 wants to merge 1 commit intomainfrom
react/provider

Conversation

@mcintyre94
Copy link
Copy Markdown
Member

@mcintyre94 mcintyre94 commented May 7, 2026

Summary of Changes

This PR is the first of a stack of additions to @solana/react to provide full react functionality for Kit apps

It adds the ClientProvider and two hooks to consume it.

ClientProvider allows you to expose any Client to the tree. It supports async clients (ie those using an async plugin), by implementing a shim of React.usefor react <19. The client is owned by the app outside of the provider lifecycle.

useClient<TClient> is a hook to get that client anywhere in the tree

useClientCapability<TClient> is the same, but with runtime checking for specific capabilities (provided by plugins) and a friendly error message to identify what's missing. This is what plugin hooks should typically use. Eg the wallet plugin would use this to provide functionality that depends on having client.wallet installed.

Copy link
Copy Markdown
Member Author

mcintyre94 commented May 7, 2026

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 7, 2026

🦋 Changeset detected

Latest commit: f473f42

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 Minor
@solana/errors Minor
@solana/accounts Minor
@solana/addresses Minor
@solana/assertions Minor
@solana/codecs-core Minor
@solana/codecs-data-structures Minor
@solana/codecs-numbers Minor
@solana/codecs-strings Minor
@solana/compat Minor
@solana/fixed-points Minor
@solana/instruction-plans Minor
@solana/instructions Minor
@solana/keys Minor
@solana/kit Minor
@solana/offchain-messages Minor
@solana/options Minor
@solana/program-client-core Minor
@solana/programs Minor
@solana/rpc-api Minor
@solana/rpc-spec Minor
@solana/rpc-subscriptions-channel-websocket Minor
@solana/rpc-subscriptions-spec Minor
@solana/rpc-subscriptions Minor
@solana/rpc-transformers Minor
@solana/rpc-transport-http Minor
@solana/rpc-types Minor
@solana/rpc Minor
@solana/signers Minor
@solana/subscribable Minor
@solana/sysvars Minor
@solana/transaction-confirmation Minor
@solana/transaction-messages Minor
@solana/transactions Minor
@solana/wallet-account-signer Minor
@solana/plugin-interfaces Minor
@solana/rpc-graphql Minor
@solana/rpc-parsed-types Minor
@solana/rpc-subscriptions-api Minor
@solana/codecs Minor
@solana/fast-stable-stringify Minor
@solana/functional Minor
@solana/nominal-types Minor
@solana/plugin-core Minor
@solana/promises Minor
@solana/rpc-spec-types Minor
@solana/webcrypto-ed25519-polyfill Minor

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
Copy link
Copy Markdown

bundlemon Bot commented May 7, 2026

BundleMon

Files updated (10)
Status Path Size Limits
react/dist/index.native.mjs
3.59KB (+512B +16.17%) -
react/dist/index.node.mjs
3.59KB (+512B +16.17%) -
react/dist/index.browser.mjs
3.59KB (+511B +16.12%) -
errors/dist/index.browser.mjs
20.69KB (+170B +0.81%) -
errors/dist/index.native.mjs
20.69KB (+170B +0.81%) -
errors/dist/index.node.mjs
20.71KB (+170B +0.81%) -
wallet-account-signer/dist/index.native.mjs
17.53KB (+165B +0.93%) -
wallet-account-signer/dist/index.browser.mjs
17.53KB (+164B +0.92%) -
wallet-account-signer/dist/index.node.mjs
17.54KB (+164B +0.92%) -
@solana/kit production bundle
kit/dist/index.production.min.js
52.27KB (+30B +0.06%) -
Unchanged files (137)
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 -
kit/dist/index.browser.mjs
3.97KB -
kit/dist/index.native.mjs
3.97KB -
kit/dist/index.node.mjs
3.97KB -
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 -
subscribable/dist/index.node.mjs
2.68KB -
subscribable/dist/index.native.mjs
2.61KB -
subscribable/dist/index.browser.mjs
2.6KB -
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.41KB -
sysvars/dist/index.browser.mjs
2.37KB -
sysvars/dist/index.native.mjs
2.37KB -
sysvars/dist/index.node.mjs
2.37KB -
transaction-confirmation/dist/index.native.mj
s
2.36KB -
transaction-confirmation/dist/index.browser.m
js
2.35KB -
rpc-subscriptions-spec/dist/index.node.mjs
2.25KB -
rpc-subscriptions-spec/dist/index.native.mjs
2.2KB -
rpc-subscriptions-spec/dist/index.browser.mjs
2.2KB -
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.91KB -
rpc-transport-http/dist/index.native.mjs
1.9KB -
rpc/dist/index.native.mjs
1.81KB -
rpc-types/dist/index.browser.mjs
1.8KB -
rpc/dist/index.browser.mjs
1.8KB -
rpc-types/dist/index.native.mjs
1.8KB -
rpc-types/dist/index.node.mjs
1.8KB -
rpc-transport-http/dist/index.node.mjs
1.72KB -
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
976B -
rpc-api/dist/index.native.mjs
975B -
rpc-api/dist/index.node.mjs
973B -
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-spec/dist/index.browser.mjs
918B -
rpc-spec/dist/index.native.mjs
918B -
rpc-spec/dist/index.node.mjs
917B -
rpc-subscriptions-api/dist/index.native.mjs
870B -
rpc-subscriptions-api/dist/index.node.mjs
869B -
rpc-subscriptions-api/dist/index.browser.mjs
868B -
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 +2.51KB +0.48%

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

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

@mcintyre94 mcintyre94 changed the base branch from react/types to graphite-base/1607 May 7, 2026 17:31
@mcintyre94
Copy link
Copy Markdown
Member Author

@trevor-cortex

@mcintyre94 mcintyre94 force-pushed the graphite-base/1607 branch from f980ae5 to 4085fd5 Compare May 8, 2026 08:19
@mcintyre94 mcintyre94 changed the base branch from graphite-base/1607 to react/types May 8, 2026 08:19
Copy link
Copy Markdown

@trevor-cortex trevor-cortex left a comment

Choose a reason for hiding this comment

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

Summary

First PR in the stack adding the React/Kit-client bindings: a ClientProvider, two consumer hooks (useClient, useClientCapability), and a usePromise shim that backports React.use to React 18. Two new errors (SOLANA_ERROR__REACT__MISSING_PROVIDER, SOLANA_ERROR__REACT__MISSING_CAPABILITY) are added in a fresh [9000000-9000999] range. The shape is clean: the provider is a thin value channel, the runtime capability check lives in one place, and the typetests pin the generic narrowing behaviour.

Overall this looks good. One real concern around Rules-of-Hooks in ClientProvider, plus a few smaller polish items below.

Things to watch out for

ClientProvider calls usePromise conditionally

const resolved = isPromiseLike(client) ? usePromise(client) : client;

This is a conditional hook call. As long as a given <ClientProvider> instance is consistently fed either a sync client or a promise it works in practice — and the README does ask for a stable reference — but:

  • It will trip react-hooks/rules-of-hooks (assuming the repo's eslint config has the rule enabled, which it typically does for a *.tsx package with a React peer dep).
  • It quietly relies on the consumer never flipping between the two shapes for one provider instance. A toggle that swaps between a precomputed sync client and createClient().use(asyncPlugin()) would crash with a hook-order mismatch — and the README's advice is about reference stability, not sync-vs-async stability.

Two straightforward fixes, either is fine:

  1. Always go through the unwrap path and have the helper short-circuit on non-thenables, e.g. usePromise(client) where usePromise returns value directly when not promise-like (no hook calls at all on that branch is also OK because the call site itself is unconditional now).
  2. Split into two components — <SyncClientProvider> and <AsyncClientProvider> — and let ClientProvider pick one based on the prop shape. Each child has a single, unconditional hook call.

I'd lean toward (1) for simplicity.

No coverage for the rejected-promise path

usePromiseShim re-throws entry.reason on subsequent renders, but there's no test that a rejected client promise surfaces to the nearest error boundary. Worth adding alongside the existing pending/fulfilled cases — both because it's a real path users will hit (failing async plugin) and because it locks in the contract against future refactors of the cache.

Note for subsequent reviewers

  • The PR depends on @solana/plugin-core being part of @solana/react's dependency graph. The README examples import createClient and Client* types from @solana/kit — if kit doesn't yet re-export from plugin-core, those snippets won't run as-is. Worth a sanity check that the umbrella exports land in the same release window as this PR, or that the docs use @solana/plugin-core directly until then.
  • Two error codes are reserved in a brand-new [9000000-9000999] range. Per CLAUDE.md these are stable forever — fine here, just calling it out so the range allocation gets a second pair of eyes.
  • usePromise.ts is intentionally not re-exported from index.ts, keeping it package-internal. Good.

Smaller polish

Inline below.

Comment thread packages/react/src/ClientProvider.tsx
Comment thread packages/react/src/usePromise.ts
Comment thread packages/react/src/usePromise.ts
if (missing.length > 0) {
throw new SolanaError(SOLANA_ERROR__REACT__MISSING_CAPABILITY, {
capabilities: missing,
hookName,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor: (name in client) will return true for inherited properties too. In practice every Kit client is built via Object.defineProperties({}, ...) so it's effectively own-properties only — but if a future plugin ever returns a class instance with capabilities on the prototype, the check would still pass while a Object.hasOwn check wouldn't. Either is defensible; flagging in case the team has a stronger opinion on the intended contract here ("capability declared" vs "capability is an own property").

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.

Updated to Object.hasOwn - matches how extendClient uses Object.getOwnPropertyDescriptors.

Comment thread packages/react/src/__tests__/ClientProvider-test.browser.tsx
Comment thread packages/react/README.md
@mcintyre94 mcintyre94 force-pushed the react/provider branch 2 times, most recently from 75196f0 to e7cf86e Compare May 8, 2026 16:05
@mcintyre94 mcintyre94 changed the base branch from react/types to graphite-base/1607 May 8, 2026 16:16
@mcintyre94 mcintyre94 force-pushed the graphite-base/1607 branch from 4085fd5 to da868aa Compare May 8, 2026 16:16
@graphite-app graphite-app Bot changed the base branch from graphite-base/1607 to main May 8, 2026 16:17
@mcintyre94 mcintyre94 force-pushed the react/provider branch 2 times, most recently from a9d79e3 to d073150 Compare May 8, 2026 16:17
@mcintyre94
Copy link
Copy Markdown
Member Author

@trevor-cortex

Copy link
Copy Markdown

@trevor-cortex trevor-cortex left a comment

Choose a reason for hiding this comment

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

Re-review — LGTM, all of my prior points have been addressed:

  • Rules-of-Hooks: ClientProvider now calls usePromise(client) unconditionally, with the runtime check moved inside usePromise. The call site is clean and won't trip the lint rule.
  • Rejected-promise coverage: new test using react-error-boundary exercises the rethrow path in usePromiseShim. Nice.
  • trackedPromise returns the chained promise: now documented inline with a clear explanation (retry runs strictly after the cache update).
  • Object.hasOwn: switched over, with ES2022.Object added to the tsconfig lib accordingly.
  • README: the duplicated suspends-via-Suspense sentence is gone.

One residual subtlety (non-blocking)

usePromise itself still has a conditional React.use call internally: on the sync branch it returns immediately and no hook is invoked, on the async branch it invokes React.use (or the shim). From the caller's perspective (ClientProvider) this is a single unconditional function call, so ESLint is satisfied — but if a given <ClientProvider> instance ever flipped between a sync client and a promise-shape client across renders, hook order would still mismatch.

In practice this can't really happen given the README's stable-reference contract and the natural lifecycle of a client, so I think this is fine as-is. Worth a one-line comment in usePromise noting the assumption ("caller must not flip between sync/async for a given instance") but I wouldn't hold the PR on it.

Notes for subsequent reviewers

  • The useClient outside-provider test (renderHook without a wrapper) will log React's noisy uncaught-error warning. Cosmetic — can be silenced with a console.error spy if it bothers anyone.
  • usePromiseShim's cache is a module-level WeakMap keyed by promise identity. Fine for the documented contract, but note that the cache persists for the module lifetime — tests that reuse promise instances across cases would need to be aware. None do today.
  • Two error codes still occupy the fresh [9000000-9000999] range. Same flag as before — worth a second pair of eyes on the range allocation.

Adds the Kit client context layer for `@solana/react`: a single provider that publishes a caller-owned Kit client to its subtree, plus two hooks — `useClient` (basic accessor with optional generic narrowing) and `useClientCapability` (runtime-checked accessor with a structured missing-capability error). Required by these hooks and by any plugin-specific hook that depends on a client capability; generic primitives like `useAction` (forthcoming) work against arbitrary async functions and don't need a provider.

The provider accepts both synchronous clients and promise-returning ones — when given a promise (e.g. `createClient().use(asyncPlugin())`) it suspends via the nearest `<Suspense>` boundary until the client resolves. On React 19 it delegates to `React.use(promise)`; on React 18 an internal thrown-promise shim, keyed by promise identity, honours the same contract.

Two new `SolanaError` codes are reserved in the `[9000000-9000999]` range: `SOLANA_ERROR__REACT__MISSING_PROVIDER` (thrown by `useClient` outside a provider) and `SOLANA_ERROR__REACT__MISSING_CAPABILITY` (thrown by `useClientCapability` with `hookName` + `providerHint` context, listing every missing capability when an array is passed).

Additive; no impact on the existing wallet-account hooks. Verified with browser + node unit tests and TS-only typetests across both `@solana/react` and `@solana/errors`.
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.

2 participants