Rework the react docs to prioritise useClient<AppClient> - #1840
Rework the react docs to prioritise useClient<AppClient>#1840mcintyre94 wants to merge 1 commit into
useClient<AppClient>#1840Conversation
|
This stack of pull requests is managed by Graphite. Learn more about stacking. |
BundleMonUnchanged files (150)
No change in files bundle size Final result: ✅ View report in BundleMon website ➡️ |
trevor-cortex
left a comment
There was a problem hiding this comment.
Docs-only rework that reorients the React guide + README around useClient<AppClient>() as the primary pattern, with useClientCapability reframed as a runtime escape hatch. The AppClient = Awaited<typeof client> idiom is a nice ergonomic default and it drops a lot of the noisy per-call ClientWithRpc<...> & ClientWithRpcSubscriptions<...> narrowing from the examples. No changeset needed — docs-only.
A few things worth a look before merging:
1. The useClientCapability example undercuts the new narrative. Both files now open the section with "you rarely need this — when you type your client with useClient<AppClient>() the compiler already guarantees the capability is present," and then immediately show a hand-rolled useCheckedRpc wrapper around useClientCapability<ClientWithRpc<GetEpochInfoApi>>. That's exactly the shape of code the prose just said you shouldn't be writing, and the example doesn't illustrate the motivating scenario the prose describes ("a loosely-typed Client read from context, say"). Consider replacing it with an example that actually shows the escape-hatch case — e.g. a helper that takes a bare Client from somewhere and asserts a capability — or at minimum a comment on the example framing it as "if you find yourself with an untyped client, here's how you'd guard". Right now the example reads as "here's the pattern" while the prose says "don't do this."
2. Please verify the generatedSigner() async claim in packages/react/README.md. The new client.ts snippet has the comment `Awaited<…>` resolves the promise when any plugin is async (e.g. `generatedSigner()`); for a fully-synchronous client it is simply `typeof client`. That parenthetical asserts generatedSigner() is async, but the same composition is used elsewhere in docs/content/docs/guides/react/index.mdx at module scope as a synchronous const client = createClient()... and the async example there wraps it in a separate createClientWithAsyncPlugins() helper. If generatedSigner() returns synchronously, the parenthetical is misleading — either drop the example or pick a plugin that really is async.
3. Two comma splices worth cleaning up:
docs/content/docs/guides/react/core-hooks.mdx: "AppClientusesAwaited<typeof client>so it resolves the promise when a plugin is async, for a fully-synchronous clientAwaited<…>is a no-op andtypeof clientalone would do." — the comma before "for a fully-synchronous client" should be a period or semicolon.packages/react/README.mduseClientintro: "It defaults to the baseClientshape, pass your client's type through the generic to get the capabilities you installed typed at the call site." — same pattern, wants a period or semicolon before "pass".
4. Code-block structure in core-hooks.mdx. The useClient example is a single twoslash block that starts as client.ts (defines and exports client + AppClient), then has an import { useClient } from '@solana/react' in the middle, then "any component" with a FetchEpochButton. Reads a bit oddly as one file. The README handles the same content with two separate code blocks (one for client.ts, one for the component importing AppClient from ./client), which is much clearer. Would be nice to align — either two twoslash blocks with a shared prelude, or a stronger visual break inside the single block. Not blocking, but the README version is easier to follow.
Notes for subsequent reviewers:
- Worth checking whether the reframe is consistent with any other places that reference
useClientCapabilityin docs (e.g. theClientProviderintro in the README still listsuseClientCapabilityin the same breath asuseClientas "Required for…"; that's fine, but if the intent is really to demote it, it may deserve a lighter mention). - The
Choosing a hooktable inindex.mdxnow describesuseClientCapabilityas "Runtime-guard a capability on an untyped client" — nice framing that matches the new narrative. - Sanity-check the twoslash snippets actually compile with the new imports (
@solana/kit-plugin-rpc,@solana/kit-plugin-signer) — these look right based on the existingindex.mdx, but a localpnpm docs:build(or however docs are built) would confirm.
|
Documentation Preview: https://kit-docs-lwj7k83rq-anza-tech.vercel.app |
ca1fd06 to
c8a53e5
Compare
Updated this, now just specifies that the
It's async
I think these read fine
This is intentional, to show |
c8a53e5 to
cd075bc
Compare
There was a problem hiding this comment.
Pull request overview
This PR updates the @solana/react documentation to prioritize a typed-client pattern (AppClient + useClient<AppClient>()) so React apps retain end-to-end type safety from their client’s actual plugin composition, while repositioning useClientCapability as an opt-in runtime assertion for loosely typed clients.
Changes:
- Updates
@solana/reactREADME examples to encourage exportingAppClient = Awaited<typeof client>and usinguseClient<AppClient>(). - Revises the React guide docs to promote typed-client usage and to de-emphasize
useClientCapabilityas the primary way to access capabilities. - Updates hook-selection guidance and examples across the React docs to reflect the typed-client approach.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| packages/react/README.md | Updates README narrative + examples to use AppClient and typed useClient<AppClient>(); reframes useClientCapability. |
| docs/content/docs/guides/react/index.mdx | Exports client and AppClient in the setup snippet and updates the “Choosing a hook” table to emphasize typed clients. |
| docs/content/docs/guides/react/core-hooks.mdx | Updates core hook docs and examples to use the AppClient pattern and repositions useClientCapability as a runtime escape hatch. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ### `ClientProvider` | ||
|
|
||
| Publishes a caller-owned Kit client to its subtree. Required for `useClient`, `useClientCapability`, and any plugin-specific hook that depends on a client capability. Generic primitives like `useAction` work against arbitrary async functions and don't need a provider. | ||
| Publishes a caller-owned Kit client to its subtree. Required for `useClient` and any plugin-specific hook that reads a client capability. Generic primitives like `useAction` work against arbitrary async functions and don't need a provider. |
| capability: 'rpc', | ||
| hookName: 'useRpc', | ||
| hookName: 'EpochBadge', | ||
| providerHint: 'Install `solanaRpc()` on the client.', |
| capability: 'rpc', | ||
| hookName: 'useRpc', | ||
| hookName: 'EpochBadge', | ||
| providerHint: 'Install a `solanaRpc()` plugin on the client.', |
| } | ||
| ``` | ||
|
|
||
| `useClient` is a pure type assertion with no runtime check. |
|
|
||
| `AppClient` uses `Awaited<typeof client>` so it resolves the promise when a plugin is async, for a fully-synchronous client `Awaited<…>` is a no-op and `typeof client` alone would do. | ||
|
|
||
| `useClient` is a pure type-cast with no runtime check. |

Problem
Initially I planned to write the react hooks around
useClientCapability, which does a runtime check for the capability on the client underClientProvider.As discussed here though, this reduces type safety which plugins otherwise maintain all the way through the stack.
We therefore plan to focus on a pattern where type safety is maintained for react apps using a typed client
We also plan to have hooks that do depend on a plugin, eg the wallet hooks, take the client with that plugin as a param: anza-xyz/kit-plugins#326. This enables type safety across the whole app.
Summary of Changes
AppClient, the type of their built client. Each component uses this in auseClient<AppClient>()useClientCapabilityis de-emphasised as a runtime escape hook, not the intended way to build client-dependent plugins<AppClient>type, instead of manually typing auseClientcall.This all fits the existing API, it's just a shift in documentation and in how we intend to build the hooks that depend on the client.
Note that I decided not to add a
createClientContextthat creates aClientProviderand a typeduseClient, I don't think that additional API surface is warranted and I think it would be more confusing.Also note that we plan to make the
useClienttype non-optional, this will be a follow up.