Skip to content

Commit b7dd2c4

Browse files
committed
docs(examples): refresh query usage and suspense guidance
1 parent 9aaff0f commit b7dd2c4

File tree

5 files changed

+269
-64
lines changed

5 files changed

+269
-64
lines changed

examples/react-hooks/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export default function App() {
5555

5656
return (
5757
<SolanaClientProvider config={clientConfig}>
58-
<SolanaQueryProvider>
58+
<SolanaQueryProvider suspense>
5959
<DemoApp connectors={walletConnectors} />
6060
</SolanaQueryProvider>
6161
</SolanaClientProvider>

examples/react-hooks/src/components/LatestBlockhashCard.tsx

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
import { useLatestBlockhash } from '@solana/react-hooks';
2+
import { Suspense } from 'react';
23

34
import { Button } from './ui/button';
45
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './ui/card';
56

67
export function LatestBlockhashCard() {
8+
return (
9+
<Suspense fallback={<LatestBlockhashFallback />}>
10+
<LatestBlockhashCardContent />
11+
</Suspense>
12+
);
13+
}
14+
15+
function LatestBlockhashCardContent() {
716
const latest = useLatestBlockhash({ refreshInterval: 30_000 });
817

918
return (
@@ -19,15 +28,14 @@ export function LatestBlockhashCard() {
1928
</CardHeader>
2029
<CardContent className="space-y-3 text-sm text-muted-foreground">
2130
<div>
22-
<span className="font-medium text-foreground">Blockhash:</span>{' '}
23-
<code>{latest.blockhash ?? 'Loading…'}</code>
31+
<span className="font-medium text-foreground">Blockhash:</span> <code>{latest.blockhash}</code>
2432
</div>
2533
<div>
2634
<span className="font-medium text-foreground">Last Valid Height:</span>{' '}
27-
{latest.lastValidBlockHeight ?? 'Unknown'}
35+
{latest.lastValidBlockHeight.toString()}
2836
</div>
2937
<div>
30-
<span className="font-medium text-foreground">Context Slot:</span> {latest.contextSlot ?? 'Unknown'}
38+
<span className="font-medium text-foreground">Context Slot:</span> {latest.contextSlot.toString()}
3139
</div>
3240
<p aria-live="polite">
3341
Status:{' '}
@@ -44,3 +52,24 @@ export function LatestBlockhashCard() {
4452
</Card>
4553
);
4654
}
55+
56+
function LatestBlockhashFallback() {
57+
return (
58+
<Card aria-busy="true">
59+
<CardHeader>
60+
<div className="space-y-1.5">
61+
<CardTitle>Latest Blockhash</CardTitle>
62+
<CardDescription>Fetching latest blockhash…</CardDescription>
63+
</div>
64+
</CardHeader>
65+
<CardContent className="text-sm text-muted-foreground">
66+
<p aria-live="polite">Loading current cluster context…</p>
67+
</CardContent>
68+
<CardFooter>
69+
<Button disabled type="button" variant="secondary">
70+
Refresh now
71+
</Button>
72+
</CardFooter>
73+
</Card>
74+
);
75+
}

examples/react-hooks/src/components/ProgramAccountsCard.tsx

Lines changed: 83 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useProgramAccounts } from '@solana/react-hooks';
2-
import { type ChangeEvent, useMemo, useState } from 'react';
2+
import { type ChangeEvent, Suspense, useMemo, useState } from 'react';
33

44
import { Button } from './ui/button';
55
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './ui/card';
@@ -9,25 +9,95 @@ const DEFAULT_PROGRAM = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr';
99

1010
export function ProgramAccountsCard() {
1111
const [program, setProgram] = useState(DEFAULT_PROGRAM);
12-
const target = program.trim();
13-
const query = useProgramAccounts(target === '' ? undefined : target, {
12+
const normalizedProgram = program.trim() === '' ? undefined : program.trim();
13+
14+
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
15+
setProgram(event.target.value);
16+
};
17+
18+
return (
19+
<Suspense
20+
fallback={
21+
<ProgramAccountsCardShell
22+
logPanel={
23+
normalizedProgram ? 'Fetching program accounts…' : 'Enter a program address to fetch accounts.'
24+
}
25+
onProgramChange={handleChange}
26+
program={program}
27+
refreshDisabled
28+
statusLabel={normalizedProgram ? 'loading' : 'idle'}
29+
/>
30+
}
31+
>
32+
<ProgramAccountsCardContent
33+
normalizedProgram={normalizedProgram}
34+
onProgramChange={handleChange}
35+
program={program}
36+
/>
37+
</Suspense>
38+
);
39+
}
40+
41+
type ProgramAccountsCardContentProps = Readonly<{
42+
normalizedProgram?: string;
43+
onProgramChange(event: ChangeEvent<HTMLInputElement>): void;
44+
program: string;
45+
}>;
46+
47+
function ProgramAccountsCardContent({ normalizedProgram, onProgramChange, program }: ProgramAccountsCardContentProps) {
48+
const query = useProgramAccounts(normalizedProgram, {
1449
config: { commitment: 'confirmed', encoding: 'base64', filters: [] },
50+
disabled: !normalizedProgram,
1551
});
1652

17-
const results = useMemo(() => {
18-
if (!query.accounts?.length) {
53+
const logPanel = useMemo(() => {
54+
if (!normalizedProgram) {
55+
return 'Enter a program address to fetch accounts.';
56+
}
57+
if (query.status === 'error' && query.error) {
58+
return `Error fetching accounts: ${formatError(query.error)}`;
59+
}
60+
if (!query.accounts.length) {
1961
return 'No accounts fetched yet.';
2062
}
2163
return query.accounts
2264
.slice(0, 5)
2365
.map((account) => `${account.pubkey.toString()} · ${account.account.data.length} bytes`)
2466
.join('\n');
25-
}, [query.accounts]);
67+
}, [normalizedProgram, query.accounts, query.error, query.status]);
2668

27-
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
28-
setProgram(event.target.value);
29-
};
69+
const refreshDisabled = !normalizedProgram;
70+
const statusLabel = query.status === 'idle' && !normalizedProgram ? 'idle' : query.status;
71+
72+
return (
73+
<ProgramAccountsCardShell
74+
logPanel={logPanel}
75+
onProgramChange={onProgramChange}
76+
onRefresh={!refreshDisabled ? () => query.refresh() : undefined}
77+
program={program}
78+
refreshDisabled={refreshDisabled}
79+
statusLabel={statusLabel}
80+
/>
81+
);
82+
}
83+
84+
type ProgramAccountsCardShellProps = Readonly<{
85+
logPanel: string;
86+
onProgramChange(event: ChangeEvent<HTMLInputElement>): void;
87+
onRefresh?: () => void;
88+
program: string;
89+
refreshDisabled: boolean;
90+
statusLabel: string;
91+
}>;
3092

93+
function ProgramAccountsCardShell({
94+
logPanel,
95+
onProgramChange,
96+
onRefresh,
97+
program,
98+
refreshDisabled,
99+
statusLabel,
100+
}: ProgramAccountsCardShellProps) {
31101
return (
32102
<Card>
33103
<CardHeader>
@@ -44,26 +114,21 @@ export function ProgramAccountsCard() {
44114
<Input
45115
autoComplete="off"
46116
id="program-address"
47-
onChange={handleChange}
117+
onChange={onProgramChange}
48118
placeholder="Program public key"
49119
value={program}
50120
/>
51121
</div>
52122
<div className="log-panel max-h-48 overflow-auto whitespace-pre-wrap" aria-live="polite">
53-
{query.status === 'error' && query.error
54-
? `Error fetching accounts: ${formatError(query.error)}`
55-
: results}
123+
{logPanel}
56124
</div>
57125
</CardContent>
58126
<CardFooter className="flex flex-wrap gap-2">
59-
<Button disabled={target === ''} onClick={() => query.refresh()} type="button" variant="secondary">
127+
<Button disabled={refreshDisabled || !onRefresh} onClick={onRefresh} type="button" variant="secondary">
60128
Refresh
61129
</Button>
62130
<span className="text-xs text-muted-foreground">
63-
Status:{' '}
64-
<span className="font-medium text-foreground">
65-
{query.status === 'idle' ? 'idle' : query.status}
66-
</span>
131+
Status: <span className="font-medium text-foreground">{statusLabel}</span>
67132
</span>
68133
</CardFooter>
69134
</Card>

examples/react-hooks/src/components/SimulateTransactionCard.tsx

Lines changed: 100 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import { useSimulateTransaction } from '@solana/react-hooks';
2-
import { type FormEvent, useState } from 'react';
2+
import { type ChangeEvent, type FormEvent, Suspense, useState } from 'react';
33

44
import { Button } from './ui/button';
55
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './ui/card';
66

77
export function SimulateTransactionCard() {
88
const [payload, setPayload] = useState('');
99
const [submittedPayload, setSubmittedPayload] = useState<string | null>(null);
10-
const simulation = useSimulateTransaction(submittedPayload ?? undefined, {
11-
disabled: submittedPayload === null,
12-
});
10+
11+
const handlePayloadChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
12+
setPayload(event.target.value);
13+
};
1314

1415
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
1516
event.preventDefault();
@@ -22,6 +23,95 @@ export function SimulateTransactionCard() {
2223
setPayload('');
2324
};
2425

26+
const canClear = payload.trim() !== '' || submittedPayload !== null;
27+
const shouldSimulate = submittedPayload !== null;
28+
29+
return (
30+
<Suspense
31+
fallback={
32+
<SimulateTransactionCardShell
33+
canClear={canClear}
34+
logOutput={shouldSimulate ? 'Simulating transaction…' : 'No simulation yet.'}
35+
onClear={handleClear}
36+
onPayloadChange={handlePayloadChange}
37+
onSubmit={handleSubmit}
38+
payload={payload}
39+
/>
40+
}
41+
>
42+
<SimulateTransactionCardContent
43+
canClear={canClear}
44+
onClear={handleClear}
45+
onPayloadChange={handlePayloadChange}
46+
onSubmit={handleSubmit}
47+
payload={payload}
48+
submittedPayload={submittedPayload}
49+
/>
50+
</Suspense>
51+
);
52+
}
53+
54+
type SimulateTransactionCardContentProps = Readonly<{
55+
canClear: boolean;
56+
onClear(): void;
57+
onPayloadChange(event: ChangeEvent<HTMLTextAreaElement>): void;
58+
onSubmit(event: FormEvent<HTMLFormElement>): void;
59+
payload: string;
60+
submittedPayload: string | null;
61+
}>;
62+
63+
function SimulateTransactionCardContent({
64+
canClear,
65+
onClear,
66+
onPayloadChange,
67+
onSubmit,
68+
payload,
69+
submittedPayload,
70+
}: SimulateTransactionCardContentProps) {
71+
const simulation = useSimulateTransaction(submittedPayload ?? undefined, {
72+
disabled: submittedPayload === null,
73+
});
74+
75+
const logOutput =
76+
simulation.status === 'idle' && !submittedPayload
77+
? 'No simulation yet.'
78+
: simulation.status === 'error'
79+
? `Simulation failed: ${formatError(simulation.error)}`
80+
: simulation.logs.length
81+
? simulation.logs.map((log, index) => `${index + 1}. ${log}`).join('\n')
82+
: 'Simulation succeeded with no logs.';
83+
84+
return (
85+
<SimulateTransactionCardShell
86+
canClear={canClear}
87+
logOutput={logOutput}
88+
onClear={onClear}
89+
onPayloadChange={onPayloadChange}
90+
onSubmit={onSubmit}
91+
payload={payload}
92+
/>
93+
);
94+
}
95+
96+
type SimulateTransactionCardShellProps = Readonly<{
97+
canClear: boolean;
98+
logOutput: string;
99+
onClear(): void;
100+
onPayloadChange(event: ChangeEvent<HTMLTextAreaElement>): void;
101+
onSubmit(event: FormEvent<HTMLFormElement>): void;
102+
payload: string;
103+
}>;
104+
105+
function SimulateTransactionCardShell({
106+
canClear,
107+
logOutput,
108+
onClear,
109+
onPayloadChange,
110+
onSubmit,
111+
payload,
112+
}: SimulateTransactionCardShellProps) {
113+
const isSimulateDisabled = payload.trim() === '';
114+
25115
return (
26116
<Card>
27117
<CardHeader>
@@ -33,7 +123,7 @@ export function SimulateTransactionCard() {
33123
</CardDescription>
34124
</div>
35125
</CardHeader>
36-
<form onSubmit={handleSubmit}>
126+
<form onSubmit={onSubmit}>
37127
<CardContent className="space-y-4">
38128
<div className="space-y-2 text-sm text-muted-foreground">
39129
<label htmlFor="simulation-wire" className="text-foreground">
@@ -42,7 +132,7 @@ export function SimulateTransactionCard() {
42132
<textarea
43133
className="min-h-[120px] w-full rounded-lg border border-border bg-background p-3 font-mono text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
44134
id="simulation-wire"
45-
onChange={(event) => setPayload(event.target.value)}
135+
onChange={onPayloadChange}
46136
placeholder="Paste a serialized transaction"
47137
value={payload}
48138
/>
@@ -51,26 +141,15 @@ export function SimulateTransactionCard() {
51141
payload, then click simulate.
52142
</p>
53143
</div>
54-
<div aria-live="polite" className="log-panel max-h-48 overflow-auto">
55-
{simulation.status === 'idle' && !submittedPayload
56-
? 'No simulation yet.'
57-
: simulation.status === 'error'
58-
? `Simulation failed: ${formatError(simulation.error)}`
59-
: simulation.logs?.length
60-
? simulation.logs.map((log, index) => `${index + 1}. ${log}`).join('\n')
61-
: 'Simulation succeeded with no logs.'}
144+
<div aria-live="polite" className="log-panel max-h-48 overflow-auto whitespace-pre-wrap">
145+
{logOutput}
62146
</div>
63147
</CardContent>
64148
<CardFooter className="flex flex-wrap gap-2">
65-
<Button disabled={payload.trim() === ''} type="submit">
149+
<Button disabled={isSimulateDisabled} type="submit">
66150
Simulate
67151
</Button>
68-
<Button
69-
disabled={!payload && !submittedPayload}
70-
onClick={handleClear}
71-
type="button"
72-
variant="ghost"
73-
>
152+
<Button disabled={!canClear} onClick={onClear} type="button" variant="ghost">
74153
Clear
75154
</Button>
76155
</CardFooter>

0 commit comments

Comments
 (0)