> ## Documentation Index
> Fetch the complete documentation index at: https://docs.0xmonaco.com/llms.txt
> Use this file to discover all available pages before exploring further.

# useInstruments

> Live market configuration via the instrument WebSocket channel

```tsx theme={null}
import { useInstruments } from "@0xmonaco/react";

const {
  instruments,
  instrumentsByPair,
  subscribed,
  error,
  clearError,
} = useInstruments();
```

A typed subscription to the public [`instrument` channel](/sdk/typescript/websockets#instruments-channel), wrapping [`sdk.ws.instruments()`](/sdk/typescript/websockets#instruments-channel). No authentication is required.

On subscribe the channel delivers a [snapshot](/sdk/typescript/websockets#subscribe-time-snapshots) of every listed market (inactive ones flagged `isActive: false`), then live change frames. Every frame carries a market's **full current configuration** rather than a delta, so the hook keeps one terminal-state entry per trading pair: each live event replaces that market's entry outright, and each snapshot **replaces the whole map** — it is the authoritative market set at the moment it was taken.

Three `change` kinds are emitted today: `listing` (a market became tradable), `delisting` (deactivated), and `config_change`. `config_change` covers a narrow set of edits — tick size, quantity step, order-size bounds, category, and a perp market's margin ladder (`riskTiers`, with `changedFields` naming `risk_tiers`). An edit to fees emits nothing. `halt` and `unhalt` are defined on `InstrumentChangeKind` for wire-forward-compatibility with a future market-regime producer and are not emitted by the admin trading-pair path — keep a `default` branch, but do not build behavior on them yet. The subscription is torn down on unmount, and re-established when `sdk.ws` or `tradingPairId` changes.

Detected broadcast lag closes the entire socket with code `1013`, including an instrument-only connection. A dropped update may belong to a market that never emits again, so continuing from the retained tail cannot repair every key. With automatic reconnect enabled, the core client resubscribes and this hook replaces its market set when a fresh instrument snapshot arrives.

A reconnect request or `onResync` callback is not proof that the snapshot succeeded. If the snapshot is unavailable, or a decision requires independently refreshed configuration, reconcile over REST. No single endpoint carries every field this channel does, so an unfiltered hook requires multiple requests:

* `sdk.market.getPaginatedTradingPairs()`, walking `page` until you have every market (one page can omit some), covers `isActive`, `tickSize`, `quantityStepSize`, `minOrderSize`, `maxOrderSize`, `category`, `marketType`, and `maxLeverage`.
* `sdk.market.getPerpMarketConfig(tradingPairId)` covers the two remaining perp fields, which the pair list does not carry — and note the rename: this channel's `initialMarginRate` / `maintenanceMarginRate` are `initialMarginRatio` / `maintenanceMarginRatio` there. It is scoped to one pair, so following every market costs one request per perp market.

That cost is why this is a deliberate reconcile — on a suspected gap, or when a decision depends on the values — rather than something to run routinely.

Each `useInstruments` call keeps its own state, so mounting the hook in two components subscribes twice. If both resolve to the **same** channel — both omitting `tradingPairId`, or both passing the same id — the second joins a channel the client already holds, which the server does not re-snapshot; the core helper replays its cached baseline to it instead. Mounts with different filters are distinct subscriptions and each get their own server snapshot. Either way the baseline arrives asynchronously, like a real snapshot, so `instruments` is empty on the first render — render an empty list, not a spinner keyed on `subscribed`.

## Parameters

<ParamField path="tradingPairId" type="string">
  Optional trading pair UUID to filter on. Omit it to follow every market. An empty string is rejected by the underlying core helper and surfaces through `error`.
</ParamField>

## Return Values

<ResponseField name="instruments" type="InstrumentEventData[]">
  Current configuration of every market seen on the channel, ordered by `symbol` and then by `tradingPairId` — a spot and a perp market sharing a symbol keep a stable order. Sorting uses plain codepoint comparison, so the order does not shift with the runtime locale.

  An inactive market stays in the list carrying `isActive: false` rather than disappearing — whether it was delisted while the subscription was live or arrived inactive in the snapshot — so a consumer can tell "no longer tradable" from "never seen" and still resolve the metadata of positions, orders and history on it. Filter on `isActive` for the tradable set. A market absent from a snapshot is a different case — it is no longer listed at all, so it leaves the list with that snapshot.
</ResponseField>

<ResponseField name="instrumentsByPair" type="Record<string, InstrumentEventData>">
  The same entries keyed by trading pair UUID, for direct lookup.
</ResponseField>

<ResponseField name="subscribed" type="boolean">
  Whether a subscription to the instrument channel is currently registered. This is **not** a connection indicator: the client accepts subscriptions while the socket is down and sends them on the next open, so `true` means "registered", not "receiving".
</ResponseField>

<ResponseField name="error" type="Error | null">
  A rejected subscription request. The core helper validates its arguments synchronously — passing an empty `tradingPairId` is the reachable case — so treat this as a bad-usage signal.

  It is deliberately **not** a transport signal: the client registers handlers regardless of socket state and logs malformed-frame failures internally, so connection loss and unparseable frames never appear here. Drive connection UI from the client's own status and resync callbacks instead.
</ResponseField>

<ResponseField name="clearError" type="() => void">
  Clear the current error state.
</ResponseField>

Each entry is an `InstrumentEventData` from `@0xmonaco/types`: `change`, `tradingPairId`, `symbol`, `isActive`, `tickSize`, `quantityStepSize`, `minOrderSize`, `maxOrderSize`, `marketType`, `category`, `changedFields`, `updatedAt`, plus the perp-only `maxLeverage`, `initialMarginRate`, and `maintenanceMarginRate` (omitted on spot markets). See the [channel reference](/reference/websockets#instrument) for the wire shape.

<Note>
  What makes the reconnect self-repairing is that the core helper delivers the snapshot as its own array rather than as per-market events, so the hook can tell where the baseline ends and drop entries missing from it. If a fresh snapshot is unavailable, the REST reconciliation above remains application-owned; reconnect does not itself fetch those endpoints.
</Note>

## Example

```tsx theme={null}
import { useInstruments } from "@0xmonaco/react";

function MarketList() {
  const { instruments, error } = useInstruments();

  if (error) return <div>Error: {error.message}</div>;

  const tradable = instruments.filter((instrument) => instrument.isActive);

  return (
    <div>
      <h2>Markets</h2>
      <ul>
        {tradable.map((instrument) => (
          <li key={instrument.tradingPairId}>
            {instrument.symbol} — tick {instrument.tickSize}, step {instrument.quantityStepSize}
            {instrument.maxLeverage ? `, up to ${instrument.maxLeverage}x` : ""}
          </li>
        ))}
      </ul>
    </div>
  );
}
```

### Following one market

```tsx theme={null}
function TickSize({ tradingPairId }: { tradingPairId: string }) {
  const { instrumentsByPair } = useInstruments(tradingPairId);
  const instrument = instrumentsByPair[tradingPairId];

  if (!instrument) return null;
  return <span>Tick size: {instrument.tickSize}</span>;
}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.