React SDK
@arca-network/react is a thin, dependency-free hooks layer over @arca-network/sdk. It wraps every watch* stream as a declarative subscription hook, adds one-shot query hooks and write/mutation hooks (with full operation/order lifecycle tracking), and ships a provider that can either adopt an existing Arca instance or bootstrap and own one for you. The only peer dependencies are react (>=18) and @arca-network/sdk — no react-query, no extra runtime.
Installation
npm install @arca-network/react @arca-network/sdk reactQuick start
Wrap your app in ArcaProvider, then call hooks anywhere beneath it. In instance mode you bring your own client and own its lifecycle:
import { Arca } from '@arca-network/sdk';import { ArcaProvider, useArcaPrice, useArcaConnection } from '@arca-network/react';
const arca = Arca.fromToken(jwt, { realm: 'my-realm' });
function App() { return ( <ArcaProvider arca={arca}> <Banner /> <BtcPrice /> </ArcaProvider> );}
function BtcPrice() { const { price, status } = useArcaPrice('hl:0:BTC'); if (status === 'loading') return <span>Loading…</span>; return <span>BTC: {price ?? '—'}</span>;}Conventions. Every watch hook returns a status of 'loading' | 'connected' | 'reconnecting' | 'error'. Live streams self-heal — they report reconnecting while the socket is down and never enter error on their own (error only means the initial subscribe threw). Coin ids are canonical ("hl:0:BTC"), and money values are decimal strings.
Provider & Connection
ArcaProvider supports two modes. Instance mode (arca={instance}) shares a client you constructed and disposed yourself. Config mode (config={...}) lets the provider build the client via Arca.fromTokenProvider / Arca.fromToken, kick off ready(), and dispose() it on unmount.
// Config mode — the provider owns the client<ArcaProvider config={{ tokenProvider: fetchToken, baseUrl, realm: 'my-realm' }}> <App /></ArcaProvider>Keep the config object stable (a constant or memoized value); the provider only rebuilds when baseUrl, realm, or the presence of token/tokenProvider changes.
useArcaConnection
Track the realtime connection / session for a banner or gate. useArcaConnection() maps the SDK's WebSocket status and auth errors to a small state machine and exposes a retry().
| Status | Meaning |
|---|---|
connecting | Establishing or re-establishing the realtime connection |
live | Connected and authenticated |
offline | Disconnected; the SDK is retrying in the background |
sessionExpired | Token refresh failed — prompt the user to re-authenticate |
function Banner() { const { status, retry } = useArcaConnection(); if (status === 'live') return null; if (status === 'sessionExpired') return <SignInPrompt />; return <button onClick={retry}>Reconnect ({status})</button>;}Watch Hooks
Each watch hook subscribes on mount, keeps state in sync with the underlying stream, and closes the stream on unmount (StrictMode-safe). They return the latest data plus a standardized status.
| Hook | Returns |
|---|---|
useArcaPrices(opts?) | { prices, status } — all mids (or narrowed by coins) |
useArcaPrice(coin) | { price, status } — single-asset convenience |
useArcaBalances(arcaRef?) | { balances, status } — live balances Map |
useArcaObject(path) | { valuation, status } — one object's valuation |
useArcaObjects(paths) | { valuations, status } — merged Map |
useArcaAggregation(sources) | { aggregation, status } |
useArcaExchangeState(objectId) | { exchangeState, pendingIntents, status } |
useArcaMaxOrderSize(opts) | { activeAssetData, status } — live max-size sizing |
useArcaFills(objectId) | { fills, status } — merged trade feed |
useArcaFunding(objectId) | { payments, status } — funding events |
useArcaOperations() | { operations, status } |
useArcaTrades(coins) | { trades, status } — public tape |
useArcaTwap(exId, opId) | { twap, status } |
useArcaCandles(coins, intervals) | { candles, status } — raw events |
useArcaCandleChart(coin, interval) | { candles, reachedStart, ensureRange, loadMore, status } |
useArcaEquityChart(path, params) | { points, status } |
usePnlChart(path, params) | { points, externalFlows, startingEquityUsd, status } |
function AccountPanel({ objectId }: { objectId: string }) { const { exchangeState, pendingIntents, status } = useArcaExchangeState(objectId); if (status === 'loading') return <Spinner />; return ( <div> <div>Equity: {exchangeState?.marginSummary.accountValue}</div> <div>{exchangeState?.positions?.length ?? 0} positions, {pendingIntents.length} pending</div> </div> );}
function PriceChart({ market }: { market: string }) { const { candles, loadMore, status } = useArcaCandleChart(market, '1m'); return <Candlestick data={candles} onScrollLeft={() => loadMore(200)} loading={status === 'loading'} />;}Query Hooks
One-shot REST reads. Each returns { data, error, loading, refetch }, re-runs when its arguments change, and preserves stale data across reloads.
| Hook | Wraps |
|---|---|
useArcaPositions(objectId) | listPositions |
useArcaOrders(objectId, status?) | listOrders |
useArcaMarketMeta() | getMarketMeta |
useArcaSparklines() | getSparklines |
useArcaObjectData(objectId) | getObjectDetail |
useArcaBalancesByPath(objectId) | getBalances |
useArcaFillsHistory(objectId, opts?) | listFills |
useArcaPnl(path, from, to) | getPnl |
useArcaEquityHistory(path, from, to) | getEquityHistory |
useArcaOperationsList(opts?) | listOperations |
useArcaCustodyStatus() | getCustodyStatus |
function Positions({ objectId }: { objectId: string }) { const { data, loading, refetch } = useArcaPositions(objectId); if (loading && !data) return <Spinner />; return ( <> <button onClick={refetch}>Refresh</button> <ul>{data?.map(p => <li key={p.market}>{p.market}: {p.size}</li>)}</ul> </> );}Mutation Hooks
Write hooks expose a status lifecycle: idle → submitting → pending → success | error, where pending means the HTTP call returned but the operation is still settling (via OperationHandle.wait()). Mutations that don't return an operation handle (e.g. useUpdateLeverage, useSetPositionTpsl) skip pending. Each returns mutate / mutateAsync, plus status, data, error, predicted, reset, and boolean flags.
| Hook | Action |
|---|---|
usePlaceOrder() | placeOrder (+ live fills & orderHandle) |
useCancelOrder() | cancelOrder |
useClosePosition() | closePosition |
useUpdateLeverage() | updateLeverage |
useSetPositionTpsl() | setPositionTpsl |
useTransfer() | transfer (with predicted balance changes) |
useEnsureArca() / useEnsureDenominatedArca() | ensureArca / ensureDenominatedArca |
useFundAccount() / useDefundAccount() | fundAccount / defundAccount |
usePlaceTwap() / useCancelTwap() | placeTwap / cancelTwap |
function BuyButton({ objectId }: { objectId: string }) { const { placeOrder, status, fills } = usePlaceOrder(); const busy = status === 'submitting' || status === 'pending'; return ( <button disabled={busy} onClick={() => placeOrder({ objectId, path: '/op/order/buy-1', market: 'hl:0:BTC', side: 'buy', size: '0.01' })} > {busy ? 'Placing…' : `Buy (${fills.length} fills)`} </button> );}
function SendButton() { const { mutate, status, predicted } = useTransfer(); return ( <button onClick={() => mutate({ path: '/op/transfer/x', from: '/a', to: '/b', amount: '10' })}> {status === 'pending' ? 'Settling…' : 'Send'} {predicted && <Hint changes={predicted.balanceChanges} />} </button> );}Operation & Order Handles
If you already hold an OperationHandle or OrderHandle (for example, you called a mutation method imperatively and stored the handle), track it reactively with useOperationHandle(handle) or useOrderHandle(handle). Both report the same status lifecycle; useOrderHandle additionally accumulates live fills. Pass null for "no operation yet".
function OrderTracker() { const arca = useArca(); const [order, setOrder] = useState<OrderHandle | null>(null); const { status, fills, data } = useOrderHandle(order);
return ( <> <button onClick={() => setOrder(arca.placeOrder({ /* ... */ }))}>Place</button> <div>Status: {status} — {fills.length} fills</div> {/* order.cancel() / order.resize('0.02') remain available on the handle */} </> );}Escape Hatches
The Arca class has ~100 methods; the named hooks cover the common ones. For anything else, two generic hooks make every method one line away.
useArcaQuery
const { data, loading, refetch } = useArcaQuery( (arca) => arca.listOrders(objectId, 'open'), [objectId],);useArcaMutation
const { mutate, status, predicted } = useArcaMutation( (arca, opts: TransferOptions) => arca.transfer(opts),);mutate({ path: '/op/transfer/x', from: '/a', to: '/b', amount: '10' });useArcaMutation auto-detects operation handles: if the factory returns an OperationHandle/OrderHandle it tracks the full submitting → pending → success lifecycle and surfaces predicted; if it returns a plain promise it goes submitting → success.
Access the client directly. useArca() returns the underlying Arca instance for any call that doesn't need React state — perfect inside event handlers.