Skip to content
For the complete documentation index optimized for AI agents, see llms.txt or llms-full.txt. A markdown version of this page is available by appending .md to the URL or sending Accept: text/markdown.

React SDK

For AI agents: the documentation index is at llms.txt (full corpus: llms-full.txt). A markdown source for this page is /sdks/react.md.

@usearete/react adds provider-managed connections and React hooks to the framework-agnostic @usearete/sdk client.

Terminal window
npm install @usearete/react @usearete/sdk zod

React remains an application peer dependency. @usearete/react carries zustand as a normal dependency, so do not install zustand separately. Generated stack files import core SDK types and Zod schemas directly, which is why generated React consumers install all three packages above.

Arete’s fluent .use(), .useOne(), and .useMutation() methods are React hooks. Standard Hooks linting cannot identify deeply nested member calls, so @usearete/react/eslint-plugin.cjs exports an arete/fluent-hooks rule for flat ESLint configurations. Enable it alongside the standard react-hooks/rules-of-hooks rule. The React ORE template includes this setup.

eslint.config.js
import areteHooks from '@usearete/react/eslint-plugin.cjs';
export default [{
plugins: { arete: areteHooks },
rules: { 'arete/fluent-hooks': 'error' },
}];

An application also needs a generated stack definition. Install the deployed public ORE stack into your source tree with the CLI:

Terminal window
a4 install ore --ts \
--output ./src/generated/ore-stack.ts \
--package-name @usearete/react

The generated entry point exports ORE_STREAM_STACK, entity types, schemas, program instructions, semantic transactions, addresses, reads, and ORE extensions. Commit generated output, but do not edit it by hand.

Wrap the application once, then call useArete with the generated stack. The stack contains its default WebSocket and HTTP endpoints, so no URL prop is required.

import { AreteProvider, useArete } from '@usearete/react';
import { ORE_STREAM_STACK } from './generated/ore-stack';
const publishableKey = import.meta.env.VITE_ARETE_PUBLISHABLE_KEY;
if (!publishableKey) throw new Error('VITE_ARETE_PUBLISHABLE_KEY is required');
function CurrentRound() {
const arete = useArete(ORE_STREAM_STACK);
const board = arete.views.OreBoard.state.use({
address: arete.addresses.board(),
});
const roundId = board.data?.state.roundId;
const round = arete.views.OreRound.state.use(
roundId == null ? undefined : { roundId },
);
if (arete.status === 'error') {
return <button onClick={() => void arete.retry()}>Reconnect</button>;
}
if (board.status !== 'ready' || round.status !== 'ready') return <p>Connecting...</p>;
return <p>Round {round.data?.id.roundId?.toString() ?? 'unavailable'}</p>;
}
export default function App() {
return (
<AreteProvider autoConnect stack={ORE_STREAM_STACK} auth={{ publishableKey }}>
<CurrentRound />
</AreteProvider>
);
}

For ORE, OreBoard/state is authoritative for the active roundId. OreRound/latest is a sorted recent-round list, not an active-round pointer. Following Board to keyed Round avoids showing a stale or future round during rollover.

Generated TypeScript uses camelCase even though stack JSON and raw view paths use snake_case:

Generated APICorrect shape
Board state key{ address: string }
Round state key{ roundId: bigint }
Miner state key{ authority: string }
Round ID fieldround.id.roundId
Deployed SOL fieldround.state.totalDeployed
Per-square SOL fieldround.state.deployedPerSquareUi
Winning square fieldround.results.winningSquare

The server view identifiers remain OreBoard/state, OreRound/state, OreRound/latest, and OreMiner/state. Use the generated object paths rather than spelling those protocol identifiers in application code.

Hosted Arete stacks require a publishable key for authentication and rate limiting. Local servers configured with allow_all may omit auth.

Read-only ORE viewing does not require a wallet, but hosted browser access is still authenticated with VITE_ARETE_PUBLISHABLE_KEY as shown above.

AreteProvider owns connected clients and their stores. Supported provider props are:

PropPurpose
stackDefault stack for argument-less useArete() calls
stackOptionsDefault lookup options for the provider stack
autoConnectStart the initial connection; defaults to true
autoReconnectRecover an established connection after loss; defaults to true
walletWallet adapter used by generated program operations
authAuthentication options such as publishableKey
fetchCustom fetch implementation
validateFramesSet false to silence rejected-frame warnings
onFrameValidationErrorObserve structured generated-schema rejections
reconnectIntervalsReconnect delays in milliseconds
maxReconnectAttemptsReconnect attempt limit
maxEntriesPerViewPer-view store limit, or null for unlimited
flushIntervalMsStore update batching interval

autoConnect and autoReconnect control separate lifecycle phases. Setting autoConnect={false} does not disable automatic recovery for a connection the app later establishes, but canRetry remains false because provider-owned replacement attempts would also stay disconnected; use the client lifecycle directly. Setting autoReconnect={false} does not prevent the provider’s initial connection.

Provider connection settings are captured when a shared client is created. Changing them does not mutate an existing client; call retry() to replace it using the latest provider configuration. Wallet updates are synchronized to existing clients in place.

With a default stack declared, components call useArete() with no arguments:

<AreteProvider autoConnect stack={MY_STACK} stackOptions={{ url: 'ws://localhost:8877' }}>
<App />
</AreteProvider>
// Anywhere below — fully typed once the stack is registered:
const arete = useArete();

Passing that same stack explicitly also keeps its provider options, so local endpoint overrides still apply: useArete(MY_STACK). Per-call options take precedence, and a different explicit stack does not inherit these defaults.

Register the stack type once so argument-less calls are typed:

declare module '@usearete/react' {
interface AreteDefaultStackRegistry {
defaultStack: MyStack;
}
}

Endpoint overrides can still go on the hook call, and always win:

const arete = useArete(MY_STACK, {
url: 'ws://localhost:8877',
httpUrl: 'http://localhost:8877',
});

useArete options are url, httpUrl, transport, and programs. Keep a supplied programs object stable with a module constant or useMemo.

Sibling components may each call useArete(MY_STACK) instead of threading one large result object through props. Calls with the same stack object and connection options share one provider-managed client and store. Concurrent initial calls also share the same connection attempt.

Changing the stack or attached-program object identity intentionally produces a different cache key. Keep generated stack imports at module scope.

Single-stack apps can avoid both repeated constants and ambient module augmentation by binding the React API once:

import { createAreteReact } from '@usearete/react';
import { ORE_STREAM_STACK } from './generated/ore-stack';
export const {
Provider: OreProvider,
useArete: useOre,
} = createAreteReact(ORE_STREAM_STACK);

useOre() carries the generated stack type directly and remains explicit at each consumer. Keep the generic AreteProvider and useArete(stack) API for multi-stack applications.

The hook returns:

  • views: generated state, list, and custom view hooks
  • programs: generated raw instructions and semantic operation hooks
  • queries: stack-level HTTP queries
  • read: callable one-shot extension reads with .use(...args, options?) React hooks
  • reads: deprecated alias of read
  • chain: generic Solana reads
  • addresses, constants, defaults, and math: generated extension surfaces
  • client: connected low-level client, or null while connecting
  • status: preferred connection state for UI, including initial client creation and failed attempts
  • connectionState, isConnected, isLoading, canRetry, and error
  • socketIssue: the latest structured WebSocket issue
  • retry(): a deduplicated manual connection retry

Program and read hooks can be called unconditionally while the client is connecting. Submitting a disconnected mutation rejects with Arete client is not connected.

const arete = useArete(ORE_STREAM_STACK);
const recent = arete.views.OreRound.latest.use({ take: 10 });
const newest = arete.views.OreRound.latest.useOne();
const allMiners = arete.views.OreMiner.list.use();

List views return arrays. .useOne() and .use({ take: 1 }) return one entity or undefined.

Each parameter set is an independent protocol v2 query. Windows and filters on the same view do not share membership, while equivalent normalized queries share one reference-counted wire subscription:

const firstPage = arete.views.OreRound.latest.use({ take: 10 });
const secondPage = arete.views.OreRound.latest.use({ take: 10, skip: 10 });

During reconnect, each hook keeps its committed data and reports isRefreshing: true. A completed authoritative snapshot atomically replaces membership for only that query, including when the new result is empty.

State views require their generated key object:

const round = arete.views.OreRound.state.use({ roundId: 335000n });
const miner = arete.views.OreMiner.state.use(
authority ? { authority } : undefined,
);

Pass undefined or null to disable a state subscription. A disabled hook returns data: undefined and isLoading: false; this is the normal wallet-disconnected pattern.

Put list query configuration in the first params object: server-side key, partition, take, skip, filters, after, withSnapshot, and snapshotLimit, plus client-side schema and onSchemaValidationError. Keep enabled and initialData in the second options object. For compatibility, query fields present in the second argument are used when absent from params; params take precedence when both are supplied.

When a caller-supplied schema rejects an entity, onSchemaValidationError(diagnostic) receives { view, key?, entity, error }. The callback adds observability only: accepted entities still pass through unchanged, and rejected entities remain excluded.

Every view hook returns a discriminated result. Checking status and isEmpty narrows data and error:

if (round.status === 'error') {
console.error(round.error); // Error
} else if (round.status === 'ready' && !round.isEmpty) {
console.log(round.data.id.roundId); // entity, not undefined
}

status separates “the client has not connected yet” (connecting) from “subscribed, waiting for the first frame” (subscribing). isPending covers both, isReady marks a usable ready result, and isEmpty identifies a ready hook with no state entity or list entries. Errors may retain previously committed data, but isReady becomes false.

initialData is trusted as immediately usable ready data. State hooks accept one entity, list hooks accept an entity array, and useOne accepts one entity. The first live store update or completed empty snapshot replaces it. An empty list settles as []; an absent state entity settles as undefined. Do not seed transaction-authorizing data unless the application is willing to trust that seed.

await result.refresh() resolves after the refreshed subscription’s next complete snapshot is committed. Registration, send, and subscription failures reject, clear isRefreshing, and are exposed through error. A view refresh with no active matching subscription is a no-op.

The view hook objects themselves are also refresh targets. They refresh that view’s active subscriptions — keyed by argument for state views, all active subscriptions when omitted — and are a no-op when nothing is subscribed:

await arete.views.OreRound.state.refresh({ roundId });
await arete.views.OreRound.state.refresh();
await arete.views.OreRound.latest.refresh();

To aggregate connection, view, and read results into loading, refreshing, and error status, use summarizeStatuses:

const streams = summarizeStatuses({ Board: board, Round: round, Miner: authority && miner });
// streams.loading → ['Board'], streams.errors → ['Treasury: socket closed']
// streams.refreshing → ['Round'] while committed data is being resynchronized

Falsy entries are skipped, which makes authority && miner the idiomatic way to gate a wallet-dependent source.

Stack extensions can expose HTTP or RPC-backed reads. React wraps each function with an argument-keyed hook:

const preview = arete.read.solClaimPreview.use(authority);
const quote = arete.read.quoteManualDeployment.use(
input,
{ debounceMs: 300 },
);

The read runs again when its arguments change and exposes data, error, status, isPending, isReady, isEmpty, isLoading, isRefreshing, and refresh(). Status is 'disabled' | 'connecting' | 'loading' | 'ready' | 'refreshing' | 'error'. It is disabled while any required argument is null or undefined. Use .use(...args, { debounceMs, initialData }) for React-only behavior with options last; debounce applies to automatic argument changes, while refresh() always runs immediately. When options follow an omitted optional read argument, pass undefined in that argument’s position. The same function remains callable for imperative access: await arete.read.solClaimPreview(authority). The old arete.reads namespace remains as a deprecated alias.

ORE’s quoteManualDeployment reads current raw Miner and Automation accounts and computes an allocation preview. A quote is not a reservation and does not guarantee the Board will stay on the same round. Transaction preparation re-reads the raw Board account and rejects a stale explicit roundId before wallet approval.

Generated raw instructions and semantic operations expose .useMutation():

const deploy = arete.programs.ore.transactions.mining
.deployWithCheckpoint
.useMutation();
await deploy.submit(
{
signer: authority,
roundId,
squares,
amountPerSquare,
},
{
reconcile: {
refresh: [
arete.views.OreBoard.state,
arete.views.OreRound.state,
arete.views.OreMiner.state,
quote,
],
},
},
);

Use mutation.mutate(args, options) from click and submit handlers when the hook’s displayError state owns failure presentation. Use the rejecting mutation.mutateAsync(args, options) or mutation.submit(args, options) for imperative workflows that await or compose the result.

refresh accepts view hook objects (which refresh that view’s active subscriptions), view and read hook results, and plain callbacks — so a component that only writes does not need its own subscriptions just to refresh them.

The mutation prepares the operation, asks the wallet to sign, submits once, and tracks transaction receipts. Each generated operation callback updates completedReceipts, signatures, and the submitted phase before the final operation receipt resolves. Arete does not receive the wallet’s private key. The SDK does not automatically retry an ambiguous submission.

Useful result fields include phase, status, prepared, result, signatures, completedReceipts, displayError, failure, reconciliationError, isPreparing, isAwaitingWallet, and isReconciling.

See Transactions for transport, inspection, and failure-outcome details.

safeToRawAmount(input, decimals) is re-exported by @usearete/react. Use it at form boundaries when invalid user input should become UI state rather than an exception:

import { safeToRawAmount } from '@usearete/react';
const parsed = safeToRawAmount({ ui: amountText }, 9);
if (!parsed.success) {
setAmountError(String(parsed.error));
return;
}
const rawAmount = parsed.data;

It returns { success: true, data } | { success: false, error }. Use the throwing toRawAmount variant when invalid input is a programmer error.

Generated program mutation hooks reconcile by default in WebSocket mode:

  1. The transaction confirms on Solana.
  2. The hook finds the highest slot in its completed receipts.
  3. It waits up to 30 seconds for the connected stack’s processed-slot watermark to reach that slot.
  4. It refreshes the supplied hook results and waits for view snapshots and one-shot reads to complete.

The watermark proves the stack processed at least that slot. It does not guarantee that every entity changed or that a one-shot read reran; include the view hook objects, hook results, or reads that need refresh.

deploy.submit(args, { reconcile: false });
deploy.submit(args, { reconcile: { timeoutMs: 10_000 } });
deploy.submit(args, { reconcile: { refresh: preview.refresh } });
deploy.submit(args, {
reconcile: async (context) => {
// A custom function replaces default processed-slot reconciliation.
},
});

onConfirmed runs after chain confirmation. onSuccess runs after successful reconciliation, or immediately when reconciliation is disabled or skipped. A reconciliation timeout, refresh failure, or refreshed snapshot timeout settles as confirmed-unreconciled, is exposed through reconciliationError, and does not reject or roll back the confirmed transaction result.

Call retryReconciliation() after a confirmed-unreconciled result to repeat only processed-slot and refresh work. The hook reuses the confirmed receipts and original refresh targets; it never rebuilds, signs, or resubmits the transaction. canRetryReconciliation indicates when that recovery is available.

HTTP-only mutations and results without receipt slots skip default stream reconciliation.

A stack build produces .arete/<Stack>.stack.json; SDK generation consumes that AST. The generated directory can contain:

  • <stack>-core.ts: generated entities, schemas, views, and program modules
  • <stack>.ts: public entry point and extension wiring
  • extension artifacts copied from the stack’s extension source
  • sdk-provenance.json: canonical input, generator, extension, and artifact hashes

Regenerate after changing a stack, IDL, extension, or generator. For this monorepo’s ORE examples:

Terminal window
bash scripts/generate-example-sdks.sh

That command rebuilds stacks/ore/.arete/OreStream.stack.json from local Arete source before emitting all example SDKs. CI runs generation twice and rejects AST, provenance, or artifact drift.

  • Legacy low-level useView and useEntity hooks are removed; generated arete.views.<Entity>.<view>.use(...) hooks are the supported subscription API.
  • Connection failures now report status: 'error'; use canRetry to decide when to show a retry action.
  • Mutation hooks add non-throwing mutate() for event handlers and keep rejecting mutateAsync() / submit() for imperative composition.
  • Install and import @usearete/react; legacy unscoped package examples are obsolete.
  • Remove the legacy provider-level WebSocket URL prop. Use generated endpoints or useArete(stack, { url, httpUrl }).
  • Pass generated state key objects such as { roundId }, { authority }, or { address }. Zero-argument state subscriptions are no longer supported.
  • Pass undefined or null to disable a state hook. Disabled hooks now report isLoading: false.
  • Use generated camelCase fields such as roundId, totalDeployed, and oreMetadata, not wire-format snake_case.
  • Generated program mutations now reconcile against the stream by default. Pass reconcile: false for confirmation-only behavior.
  • Use onConfirmed for work that must happen after chain confirmation even if reconciliation later times out; reserve onSuccess for reconciled success.
  • One-shot extension reads expose imperative calls and hooks together under arete.read.<name>; arete.reads remains a deprecated alias.
  • Repeated useArete calls share a client when their stack and option identities match. Remove component-local connection ownership workarounds.

@usearete/react exports useConnectionState, useAreteContext, useAsyncRead, useInstructionMutation, reconciliation helpers, and selected core SDK APIs and types. Generated view hooks are the supported React subscription API. The package is not a complete mirror of @usearete/sdk; import the core package directly for the full low-level surface.