Skip to content

React Hooks ​

market-feed/react provides five hooks for integrating live market data into React and React Native applications.

Installation ​

React 18 or later is required as a peer dependency:

bash
npm install market-feed react

useQuote ​

Fetches a live quote for a single symbol, polling at a configurable interval.

tsx
import { useQuote } from "market-feed/react";
import { MarketFeed } from "market-feed";

const feed = new MarketFeed();

function StockTicker({ symbol }: { symbol: string }) {
  const { data, loading, error } = useQuote(feed, symbol, { intervalMs: 10_000 });

  if (loading) return <span>Loading…</span>;
  if (error)   return <span>Error: {error.message}</span>;

  return (
    <div>
      <strong>{data?.symbol}</strong> ${data?.price.toFixed(2)}
      {" "}
      <span style={{ color: (data?.changePercent ?? 0) >= 0 ? "green" : "red" }}>
        {data?.changePercent.toFixed(2)}%
      </span>
    </div>
  );
}

Options ​

ts
interface UseQuoteOptions {
  /** Poll interval in milliseconds. Default: 30 000 */
  intervalMs?: number;
  /** Skip fetching when false. Default: true */
  enabled?: boolean;
}

Return value ​

ts
interface UseQuoteResult {
  data: Quote | null;
  loading: boolean;
  error: Error | null;
}

The hook re-fetches automatically whenever symbol, intervalMs, or enabled changes. A fresh fetch fires immediately on mount and on each symbol change — the interval counter resets.


useStream ​

Subscribes to a live event stream via watch() from market-feed/stream. Yields QuoteEvent, MarketOpenEvent, and DivergenceEvent objects.

tsx
import { useStream } from "market-feed/react";
import { MarketFeed } from "market-feed";
import type { QuoteEvent } from "market-feed/stream";

const feed = new MarketFeed();

function LivePrices({ symbols }: { symbols: string[] }) {
  const { event, error } = useStream(feed, symbols);

  if (!event) return <p>Waiting for data…</p>;

  if (event.type === "quote") {
    const q = (event as QuoteEvent).quote;
    return <p>{q.symbol}: ${q.price.toFixed(2)}</p>;
  }

  if (event.type === "market_open") return <p>Market opened</p>;
  if (event.type === "market_close") return <p>Market closed</p>;

  return null;
}

Options ​

ts
interface UseStreamOptions {
  /** Poll interval in ms when market is open. Default: 5 000 */
  intervalMs?: number;
  /** Multiplier applied to intervalMs when market is closed. Default: 60 */
  closedMultiplier?: number;
  /** Exchange to check for market hours. Default: "NYSE" */
  exchange?: string;
  /** Whether to skip off-hours polling. Default: true */
  respectMarketHours?: boolean;
}

Return value ​

ts
interface UseStreamResult {
  event: WatchEvent | null;
  error: Error | null;
}

The stream restarts automatically whenever feed reference or the symbols list (compared by content) changes. An AbortController is used for cleanup on unmount.


useAlerts ​

Runs watchAlerts() and accumulates fired AlertEvent objects.

tsx
import { useAlerts } from "market-feed/react";
import type { AlertConfig } from "market-feed/alerts";
import { MarketFeed } from "market-feed";

const feed = new MarketFeed();

const alerts: AlertConfig[] = [
  { symbol: "AAPL", condition: { type: "price_above", threshold: 200 }, once: true },
  { symbol: "TSLA", condition: { type: "change_pct_below", threshold: -5 }, debounceMs: 300_000 },
];

function AlertLog() {
  const { events, error } = useAlerts(feed, alerts, { intervalMs: 10_000 });

  return (
    <ul>
      {events.map((ev, i) => (
        <li key={i}>
          [{ev.triggeredAt.toLocaleTimeString()}] {ev.alert.symbol}{" "}
          {ev.alert.condition.type} — ${ev.quote.price.toFixed(2)}
        </li>
      ))}
    </ul>
  );
}

Options ​

ts
interface AlertsOptions {
  /** Poll interval in milliseconds. Default: 5 000 */
  intervalMs?: number;
}

Return value ​

ts
interface UseAlertsResult {
  events: AlertEvent[];   // accumulates over component lifetime
  error: Error | null;
}

Fired events accumulate in the events array — they are never removed. The generator restarts (and events is cleared) whenever the alerts list changes (compared by content).


useWebSocket ​

Subscribes to a real-time WebSocket stream via connect() from market-feed/ws. Works on React Native — WebSocket is available natively in all RN versions.

tsx
import { useWebSocket } from "market-feed/react";
import { FinnhubProvider } from "market-feed";

const provider = new FinnhubProvider({ apiKey: process.env.EXPO_PUBLIC_FINNHUB_KEY! });

function LiveTrades({ symbols }: { symbols: string[] }) {
  const { event, latestTrade, error } = useWebSocket(provider, symbols);

  if (error) return <Text>Stream error: {error.message}</Text>;
  if (!latestTrade) return <Text>Connecting…</Text>;

  return (
    <Text>
      {latestTrade.symbol}: ${latestTrade.price.toFixed(2)} × {latestTrade.size}
    </Text>
  );
}

Options ​

ts
interface WsOptions {
  /** Custom WebSocket constructor (for Node 18–20). Not needed in React Native. */
  wsImpl?: typeof globalThis.WebSocket;
  /** Max reconnect attempts. Default: 10 */
  maxReconnectAttempts?: number;
  /** Base reconnect delay in ms. Default: 1000 */
  reconnectDelayMs?: number;
}

Return value ​

ts
interface UseWebSocketResult {
  /** The latest WsEvent (trade, connected, disconnected, error) — or null. */
  event: WsEvent | null;
  /** Shortcut for the most recent trade, or null before the first trade arrives. */
  latestTrade: WsTrade | null;
  /** Fatal error if the stream throws unexpectedly. */
  error: Error | null;
}

The stream restarts automatically when the symbols list changes. It is stopped on unmount via an internal AbortSignal.


useOrderBook ​

Subscribes to top-of-book bid/ask updates via getOrderBook() from market-feed/ws. Works on React Native.

tsx
import { useOrderBook } from "market-feed/react";
import { PolygonProvider } from "market-feed";

const provider = new PolygonProvider({ apiKey: process.env.EXPO_PUBLIC_POLYGON_KEY! });

function OrderBook({ symbol }: { symbol: string }) {
  const { orderBook, error } = useOrderBook(provider, symbol);

  if (error) return <Text>Error: {error.message}</Text>;
  if (!orderBook) return <Text>Waiting for quotes…</Text>;

  const bid = orderBook.bids[0];
  const ask = orderBook.asks[0];

  return (
    <View>
      <Text>Bid: ${bid?.price.toFixed(2)} × {bid?.size}</Text>
      <Text>Ask: ${ask?.price.toFixed(2)} × {ask?.size}</Text>
    </View>
  );
}

Supported providers ​

ProviderData source
PolygonProviderNative WS — Q.* NBBO quotes
AlpacaProviderNative WS — quotes channel
IbTwsProviderNative WS — bid/ask fields
All othersPolling fallback — synthesises spread from quote()

Return value ​

ts
interface UseOrderBookResult {
  /** Latest top-of-book snapshot, or null before the first update. */
  orderBook: OrderBookEvent | null;
  /** Fatal error if the generator throws unexpectedly. */
  error: Error | null;
}

Stable references ​

All three hooks keep refs to source/feed and options internally, so passing a new object reference on every render does not restart the subscription. Only changes to key values (symbol, symbols content, alerts content, intervalMs, enabled) trigger a restart.

tsx
// Safe: new object each render, but key values unchanged → no restart
const { data } = useQuote(feed, "AAPL", { intervalMs: 10_000 });

// Triggers restart: symbol changed
const { data } = useQuote(feed, ticker, { intervalMs: 10_000 });

Server-side rendering ​

The hooks are client-only. When rendering in a server environment (Next.js App Router, Remix), guard with "use client" or dynamic imports:

tsx
// components/Ticker.tsx
"use client";

import { useQuote } from "market-feed/react";
// ...

Or with Next.js dynamic imports:

tsx
const Ticker = dynamic(() => import("./Ticker"), { ssr: false });

Using a single provider ​

Like other modules, the hooks accept any object with a quote() method — not just MarketFeed:

tsx
import { YahooProvider } from "market-feed";

const yahoo = new YahooProvider();
const { data } = useQuote(yahoo, "AAPL");

React Native ​

All five hooks work in React Native (Expo and bare workflow). React Native provides fetch, WebSocket, and AbortController natively, so no polyfills are needed on modern versions.

Setup (Expo) ​

bash
npx expo install market-feed react
tsx
import { useQuote, useWebSocket } from "market-feed/react";
import { FinnhubProvider } from "market-feed";

const provider = new FinnhubProvider({
  apiKey: process.env.EXPO_PUBLIC_FINNHUB_KEY!,
});

export default function App() {
  const { data } = useQuote(provider, "AAPL", { intervalMs: 10_000 });
  const { latestTrade } = useWebSocket(provider, ["AAPL", "MSFT", "TSLA"]);

  return (
    <View>
      <Text>REST: ${data?.price.toFixed(2)}</Text>
      <Text>WS: ${latestTrade?.price.toFixed(2)}</Text>
    </View>
  );
}

AbortController ​

AbortController is available natively since React Native 0.71. For older versions, install the polyfill:

bash
npm install abortcontroller-polyfill
ts
// index.js — before any other imports
import "abortcontroller-polyfill/dist/abortcontroller-polyfill-only";

Metro bundler ​

No special Metro config is needed. market-feed ships both ESM and CJS, and Metro resolves CJS by default.

Released under the MIT License.