npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@taphubhq/sdk-react

v21.1.0

Published

React bindings for the TabHub Core SDK

Downloads

7,259

Readme

@taphubhq/sdk-react

React 18/19 bindings for @taphubhq/sdk-core.

Install

pnpm add @taphubhq/sdk-react @taphubhq/sdk-core @tanstack/react-query zustand lightweight-charts

Minimum Integration

import { TaphubProvider } from '@taphubhq/sdk-react';
import { TaphubClient } from '@taphubhq/sdk-core';

const client = new TaphubClient({
  agencyId: 'acme-corp',
  endpoint: 'https://api.example.com',
});

function App() {
  return (
    <TaphubProvider client={client}>
      <YourApp />
    </TaphubProvider>
  );
}

Hook Example

import { useGame } from '@taphubhq/sdk-react';

function GameInfo({ pair }: { pair: string }) {
  const { data, isLoading } = useGame(pair);

  if (isLoading) return <div>Loading...</div>;
  return <div>{data?.pair} — {data?.status}</div>;
}

Component Example

import { BidPanel } from '@taphubhq/sdk-react';

function TradingView() {
  return <BidPanel pair="BTC-USDT" />;
}

Tailwind Preset

To use the bundled Tailwind preset, add it to your Tailwind config:

// tailwind.config.js
module.exports = {
  presets: [require('@taphubhq/sdk-react/tailwind.preset')],
  content: ['./node_modules/@taphubhq/sdk-react/dist/**/*.{js,mjs}'],
};

Locales (i18n)

Runtime UI translations served by your backend's Query.locales operation, cached in-memory, with a bundled English baseline that renders before any network round-trip completes.

Provider config

TaphubProvider accepts three new props that drive locale lifecycle:

<TaphubProvider
  client={client}
  lang="en"                // active language code; default 'en'
  pollIntervalMs={30000}   // polling interval in ms; default 30000, min 5000
  localeEnabled={true}     // toggle locale polling on/off; default true
>
  <App />
</TaphubProvider>

All three props are optional. Omitting them mounts the provider with default locale behaviour: bundled English baseline + 30s polling against the backend configured in TaphubClient.

Reading translations — useT

import { useT } from '@taphubhq/sdk-react';

function Greeting() {
  const title = useT('home.title');
  const cta = useT('home.cta', 'Get started'); // optional fallback arg
  return (
    <header>
      <h1>{title}</h1>
      <button type="button">{cta}</button>
    </header>
  );
}

useT(key, fallback?) lookup order (always returns a non-empty string):

  1. Active-language translations map
  2. Bundled English baseline shipped with the SDK
  3. fallback argument if provided
  4. key itself (last-resort, visible-but-broken signal for missing keys)

Reading + switching language — useLocale

import { useLocale } from '@taphubhq/sdk-react';

function LangSwitcher() {
  const { lang, version, status, switchLang } = useLocale();
  return (
    <div>
      <span>Active: {lang}</span>
      <span>Status: {status}</span>
      <button type="button" onClick={() => switchLang('zh-TW')}>繁中</button>
      <button type="button" onClick={() => switchLang('en')}>English</button>
    </div>
  );
}

status is one of:

  • 'bundled' — serving the shipped English baseline (cold start or just-switched language)
  • 'live' — serving translations pulled from the backend; version is set
  • 'stale-served' — last live data still rendered, but most recent fetch failed (network, FeatureDisabled, InsufficientUpstream, etc.). Consumer can decide whether to suppress translation-dependent UI.

Bundled English source

The bundled baseline lives at packages/sdk-react/src/i18n/en.json. Refresh it from the live Google Sheet at SDK publish time — the bundled file is the floor that renders when backend is unreachable, so it should track the current Sheet to avoid stale-looking English on cold start.

A helper script lives at scripts/sync-bundled-en.mjs. Run before pnpm changeset:

SYNC_GQL_URL=https://your-backend/grid-gql \
SYNC_GQL_TOKEN=<bearer> \
SYNC_AGENCY_ID=<your-agency> \
  pnpm sync:bundled-en

The script fetches Query.locales(input: { lang: "en" }), parses the JSON-string translations, and overwrites src/i18n/en.json. It logs the diff (added / removed / changed keys) and exits non-zero on any failure (unreachable backend, gql error, empty response). No silent overwrites.

Failure handling

The provider never throws to the render tree. Network errors, gql errors with extensions.code: "FeatureDisabled", "InsufficientUpstream", "LangNotSupported", or any other rejection flip the store to 'stale-served' and keep the prior translations rendering. Upstream-side contract: see the n26-side handoff doc docs/multilang-locales.md.

Chart

The ChartContainer component renders a lightweight-charts@^5 candlestick chart with an interactive bid grid overlay. It pulls historical candles via REST (useChartHistory) and subscribes to MQTT candle ticks via useCandleHandler, writing all state into the game store managed by TaphubProvider.

pnpm add lightweight-charts

Minimum Integration

import { ChartContainer, TaphubProvider, useSyncGameStore } from '@taphubhq/sdk-react';
import { TaphubClient } from '@taphubhq/sdk-core';
import '@taphubhq/sdk-react/theme.css';

const client = new TaphubClient({
  agencyId: 'acme-corp',
  endpoint: 'https://api.example.com',
  mqttEndpoint: 'wss://stream.example.com/ws',
});

function TradingView({ gameId }: { gameId: string }) {
  useSyncGameStore(gameId);

  const placeBid = async ({ time1, time2, price1, price2, coefficient }) => {
    const ok = await client.bid.place({ gameId, time1, time2, price1, price2, coefficient });
    return ok;
  };

  return (
    <div style={{ width: '100%', height: 600, position: 'relative' }}>
      <ChartContainer gameId={gameId} placeBid={placeBid} />
    </div>
  );
}

function App() {
  return (
    <TaphubProvider client={client}>
      <TradingView gameId="V_ETH/USD" />
    </TaphubProvider>
  );
}

Props

| Prop | Type | Description | |---|---|---| | gameId | string \| undefined | Game identifier. Drives REST history fetch and MQTT subscription. | | placeBid | (p: { time1; time2; price1; price2; coefficient }) => Promise<boolean> | Called when user clicks a cell. Return true if accepted; on false the optimistic cell is removed. | | className | string? | Overrides default position:relative; width:100%; height:100% wrapper styles. | | onReady | (refs: ChartContainerRefs) => void? | Fired after the grid primitive is attached. Re-fires on structural re-attach. Use for debug/E2E instrumentation (see below). |

Notes

  • Parent container must have a non-zero height (e.g. height: 600px or flex child) — the chart fills its parent.
  • gridConfig, constraints, acceptableBids, orders, bidAmount, userId, isDemo are read from the store via TaphubProvider. Keep them in sync with useSyncGameStore or by dispatching the store actions yourself.
  • Listens for window event 'center-chart' — dispatch it from a toolbar button to re-center the chart.

Accessing Internal Refs (Debug / E2E)

Use onReady to reach the underlying IChartApi, candlestick series, and CustomGridPrimitive — useful for Playwright E2E bridges or manual instrumentation. The callback is multi-shot: it re-fires whenever the primitive is re-attached due to grid structure changes.

import { ChartContainer, type ChartContainerRefs } from '@taphubhq/sdk-react';

function TradingView({ gameId }) {
  const handleReady = (refs: ChartContainerRefs) => {
    window.tap = {
      chart: refs.chart,
      series: refs.series,
      primitive: refs.primitive,
    };
  };

  return (
    <div style={{ width: '100%', height: 600, position: 'relative' }}>
      <ChartContainer gameId={gameId} placeBid={placeBid} onReady={handleReady} />
    </div>
  );
}

Note: Always refresh your references on each onReady call — primitive is recreated on structural re-attach and a stale reference will silently break.

Deprecated

PriceChart, CandleGrid, useCandleGrid, useCandleGridHook, useCandleGridRecenter, useCandleGridPushCandles, and useCandleGridStateFromStore are deprecated in favour of ChartContainer and will be removed in a future release.

Grid Primitive (Advanced)

For direct control over the grid overlay, use CustomGridPrimitive:

import { CustomGridPrimitive, createCustomGridOptions, centerChart } from '@taphubhq/sdk-react';

const gridOptions = createCustomGridOptions(serverGridConfig, constraints, (cell) => {
  console.log('Cell clicked:', cell);
  return true;
});

series.attachPrimitive(new CustomGridPrimitive(gridOptions, initialBids));

// Center chart on current price
centerChart(chart, candles, serverGridConfig);

// Bid lifecycle
primitive.bindBidId(bidId, coords);
primitive.updateBid({ id: bidId, status: 'won', payout: '12.5', ...coords });
primitive.removeBid(coords);
primitive.addBid(otherUserBid, avatarUrl);

Coefficient Calculation

The SDK now includes full GBM probability math matching the Go backend:

import { calculateCoefficientWrapper, computeCellSizeValue, computeBaseline } from '@taphubhq/sdk-react';

const coef = calculateCoefficientWrapper({
  time1, time2, price1, price2,
  candleTime, candleClose, volatility,
  coefMults, cellSizeTime, candleSize,
});

const cellSize = computeCellSizeValue(price, volatility);
const { baseline, baselineTime } = computeBaseline(close, timeSec, cellSize, cellSizeTime, candleSize);

Theming

Chart and grid colours are driven by CSS custom properties. Override them in a higher-specificity selector:

:root {
  --taphub-cell-won: 10 200 50;
  --taphub-grid-line: 40 40 40;
  --canvas-bid-tier-0-fill: rgba(60, 122, 255, 0.3);
  --canvas-bid-tier-0-border: rgba(60, 122, 255, 0.6);
  --canvas-cell-won-text: rgba(255, 255, 255, 1);
}

The Tailwind preset exposes matching colour tokens (bg-taphub-cell-won, text-taphub-coefficient, etc.) that reference the same CSS variables.

Bid Surface

The SDK ships BidForm, BidHistory, and BidPanel components for the bid workflow.

Minimum Integration

import { BidPanel, TaphubProvider } from '@taphubhq/sdk-react';
import { TaphubClient } from '@taphubhq/sdk-core';

const client = new TaphubClient({ agencyId: 'acme-corp', endpoint: 'https://api.example.com' });

function App() {
  return (
    <TaphubProvider client={client}>
      <BidPanel pair="BTC-USDT" />
    </TaphubProvider>
  );
}

Headless Composition

Use BidForm and BidHistory independently with useBidFormFromStore:

import { BidForm, useBidFormFromStore, useSyncGameStore } from '@taphubhq/sdk-react';

function MyBidUI({ pair }: { pair: string }) {
  const formProps = useBidFormFromStore();
  useSyncGameStore(pair);

  return (
    <BidForm
      pair={pair}
      {...formProps}
      selectedCell={mySelectedCell}
      onBidPlaced={(bid) => console.log('Bid placed:', bid)}
    />
  );
}

Syncing Realtime Events

useSyncGameStore wires MQTT configUpdate and balanceUpdate events into the store automatically:

import { useSyncGameStore } from '@taphubhq/sdk-react';

function GameView({ gameId }: { gameId: string }) {
  useSyncGameStore(gameId);
  // The store now reflects min/max bid amounts, balance, etc.
}

Bid Result Lifecycle

useBidResult subscribes to the MQTT bid_result and balance_update channel events and drives the full win/lose lifecycle automatically:

  • Won → calls resolveOrder(id, 'won', payout)ChartContainer shows confetti on the winning cell.
  • Lost → calls resolveOrder(id, 'lost') → cell dims to inactive state.
  • Cancelled → no store mutation; fires onRejected callback.
  • Balance update → syncs store balance (guards against stale debit events).

Mount it alongside ChartContainer at the page level:

import { ChartContainer, useBidResult, useSyncGameStore } from '@taphubhq/sdk-react';

function TradingView({ gameId }: { gameId: string }) {
  useSyncGameStore(gameId);

  useBidResult(gameId, {
    onWon: ({ bidId, payout }) => {
      playWinSound();
      showToast(`+${payout} USDT`);
    },
    onLost: ({ bidId }) => {
      playLossSound();
    },
    onRejected: ({ bidId, reason }) => {
      showToast(reason ?? 'Bet rejected', { type: 'error' });
    },
  });

  return (
    <div style={{ width: '100%', height: 600, position: 'relative' }}>
      <ChartContainer gameId={gameId} placeBid={placeBid} />
    </div>
  );
}

Callbacks

All callbacks are optional. Omit any you don't need.

| Callback | Payload | When | |---|---|---| | onWon | { bidId, payout? } | After resolveOrder('won') is dispatched | | onLost | { bidId } | After resolveOrder('lost') is dispatched | | onRejected | { bidId, reason? } | On cancelled status — no store change |

Note: reason is undefined until the wire protocol carries it; the field is reserved for future use.

Callback Stability

Callback identity is captured in a ref — passing a new inline function on each render does not re-subscribe the channel listener.

Cell Size Control

The cell-size control shows the active grid cell height and, when the server suggests a better value, lets the user accept it. Three integration tiers, easiest first.

Tier 1 — CellSizeButton (self-contained)

Drop-in. Brings its own floating layer (base-ui tooltip on mouse, popover on touch) — no host floating library needed.

import { CellSizeButton } from '@taphubhq/sdk-react';

function Toolbar({ cellSize, suggested }: { cellSize: number; suggested: number | null }) {
  return (
    <CellSizeButton
      currentValue={cellSize}
      idealValue={suggested} // null = no suggestion → info-only
      onAccept={() => applyCellSize(suggested!)}
      onDismiss={() => dismissSuggestion()}
    />
  );
}

Tier 2 — compound atoms with your own floating layer

If your app already uses a floating library (e.g. shadcn / Radix), compose the face (CellSizeButtonTrigger) and the content (CellSizeUpdater) yourself. The trigger is a <span> that forwards ref + props, so the host's TooltipTrigger/PopoverTrigger can wire it.

import { CellSizeButtonTrigger, CellSizeUpdater } from '@taphubhq/sdk-react';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';

function CellSize({ cellSize, suggested, onAccept, onDismiss }) {
  return (
    <Tooltip>
      <TooltipTrigger asChild>
        {/* style is spread last → override cursor, etc. */}
        <CellSizeButtonTrigger
          currentValue={cellSize}
          idealValue={suggested}
          style={{ cursor: 'pointer' }}
        />
      </TooltipTrigger>
      <TooltipContent data-taphub-scope>
        <CellSizeUpdater
          currentValue={cellSize}
          idealValue={suggested}
          onAccept={onAccept}
          onDismiss={onDismiss}
        />
      </TooltipContent>
    </Tooltip>
  );
}

Wrap SDK content in data-taphub-scope so the SDK's theme tokens (--surface-raised, --primary, …) resolve inside your portal.

Tier 3 — split atoms (value and info icon open different surfaces)

When the value and the (i) icon must trigger separate floating surfaces — value → accept popup, icon → explanation tooltip — use the granular atoms. CellSizeButtonTrigger renders both in one span and can't separate the click targets; these can.

| Atom | Renders | Pair with | |---|---|---| | CellSizeValue | value face only ({cur} or shimmering {cur} → {ideal}) | accept popup → CellSizeUpdater | | CellSizeInfoIcon | the (i) glyph only | explanation tooltip → CellSizeInfo | | CellSizeInfo | suggestion-aware info copy | inside the info tooltip |

import {
  CellSizeValue,
  CellSizeInfoIcon,
  CellSizeInfo,
  CellSizeUpdater,
} from '@taphubhq/sdk-react';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';

function CellSize({ cellSize, suggested, onAccept, onDismiss }) {
  return (
    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
      {/* value → accept popup (click) */}
      <Popover>
        <PopoverTrigger asChild>
          <CellSizeValue currentValue={cellSize} idealValue={suggested} style={{ cursor: 'pointer' }} />
        </PopoverTrigger>
        <PopoverContent data-taphub-scope>
          <CellSizeUpdater
            currentValue={cellSize}
            idealValue={suggested}
            onAccept={onAccept}
            onDismiss={onDismiss}
          />
        </PopoverContent>
      </Popover>

      {/* (i) icon → explanation tooltip (hover/click). CellSizeInfo is
          suggestion-aware: it appends "You can switch to {ideal}…" only when
          `suggested` is a pending suggestion. */}
      <Tooltip>
        <TooltipTrigger asChild>
          <CellSizeInfoIcon />
        </TooltipTrigger>
        <TooltipContent data-taphub-scope>
          <CellSizeInfo currentValue={cellSize} idealValue={suggested} />
        </TooltipContent>
      </Tooltip>
    </span>
  );
}

API Reference

See the OpenSpec specs for detailed requirements and scenarios.

Leaderboard and Orders

useLeaderboard

Deprecated. Use useRankBoard. The backend leaderboard query is deprecated in favour of rank_leaderboardV2.

Fetches the platform leaderboard for a given period and sort column. Returns a TanStack Query result.

import { useLeaderboard } from '@taphubhq/sdk-react';

function LeaderboardTable() {
  const { data, isLoading } = useLeaderboard({ period: 'weekly', sortBy: 'gain' });

  if (isLoading) return <div>Loading...</div>;
  return (
    <ul>
      {data?.map((entry) => (
        <li key={entry.userId}>{entry.rank}. {entry.username} — {entry.gain}</li>
      ))}
    </ul>
  );
}

For frequently-refreshing leaderboards, set a staleTime via TanStack Query options to avoid excessive polling:

const { data } = useLeaderboard({ period: 'weekly', sortBy: 'gain' }, {
  staleTime: 30_000,
});

useRankBoard (v2)

Fetches the duration-scoped rank board (rank_leaderboardV2). A single-page query: you drive offset/limit, total gives "#X of N", and me is the caller's own row+rank (null when anonymous). Returns a TanStack Query result holding a RankBoard.

import { useRankBoard } from '@taphubhq/sdk-react';

function RankBoardTable() {
  const [offset, setOffset] = useState(0);
  const limit = 50;
  const { data, isLoading } = useRankBoard({ period: 'last_7d', sort: 'pnl', limit, offset });

  if (isLoading || !data) return <div>Loading...</div>;
  return (
    <>
      <ul>
        {data.entries.map((e) => (
          <li key={e.userId}>{e.rank}. {e.username} — pnl {e.pnl} / vol {e.vol}</li>
        ))}
      </ul>
      {data.me && <p>You are #{data.me.rank} of {data.total} (pnl {data.me.pnl})</p>}
      <button disabled={offset === 0} onClick={() => setOffset((o) => Math.max(0, o - limit))}>Prev</button>
      <button disabled={offset + limit >= data.total} onClick={() => setOffset((o) => o + limit)}>Next</button>
    </>
  );
}

For frequently-refreshing boards, set a staleTime on your QueryClient defaults (or the ['taphub', 'rankBoard'] key) to avoid excessive polling, e.g. defaultOptions: { queries: { staleTime: 30_000 } } when creating the client.

The custom period needs an hour-aligned UTC range (from/to as ISO 8601 strings); preset periods ignore range. me is populated only for authenticated callers — anon visitors get the board with me: null.

useRankBoard({
  period: 'custom',
  range: { from: '2026-06-01T00:00:00Z', to: '2026-06-02T00:00:00Z' },
  sort: 'vol',
  sortDir: 'desc',
});

useActiveBids

Fetches the authenticated user's pending bids once after auth and hydrates them into useGameStore via addOrder. Use it once near your app root so pending bids survive page refresh.

import { useActiveBids } from '@taphubhq/sdk-react';

function HydratePendingBids() {
  useActiveBids();
  return null;
}

The hook is a no-op until useGameStore.authToken is set. Errors are logged to console.error and swallowed.

useOrders

Paginates the authenticated user's order history using useInfiniteQuery. Returns { orders, hasMore, fetchNextPage, ... }.

import { useOrders } from '@taphubhq/sdk-react';

function OrdersList() {
  const { orders, hasMore, fetchNextPage, isFetchingNextPage } = useOrders({ filter: 'all' });

  return (
    <div>
      {orders.map((order) => (
        <div key={order.id}>{order.status} — {order.amount}</div>
      ))}
      {hasMore && (
        <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
          Load more
        </button>
      )}
    </div>
  );
}

useWallets / useWalletByCurrency

Read the current user's wallet balances via the user-service GraphQL endpoint. Thin wrappers around TanStack Query.

import { useWallets, useWalletByCurrency } from '@taphubhq/sdk-react';
import type { UserWallet } from '@taphubhq/sdk-react';

function BalancePanel() {
  const { data: wallets, isLoading } = useWallets();
  if (isLoading) return <span>Loading…</span>;
  return (
    <ul>
      {wallets?.map((w: UserWallet) => (
        <li key={w.id}>{w.currency}: {w.amount}</li>
      ))}
    </ul>
  );
}

function UsdcBalance({ currency }: { currency: string }) {
  // Auto-disables when `currency` is empty / undefined.
  const { data: wallet } = useWalletByCurrency(currency);
  return <span>{wallet?.amount ?? '—'}</span>;
}

Both hooks accept an optional { enabled?: boolean } second argument and forward the TanStack Query AbortSignal to the underlying GraphQL call so unmount cancels in-flight requests. Cache keys: ['taphub','wallets'] and ['taphub','wallet',currency].

Note: wallet.amount is a string (preserves Decimal precision on the wire). Do not coerce blindly via Number(...) / parseFloat(...) for large or precision-sensitive balances — use a Decimal library (big.js, decimal.js) if exact arithmetic matters.

This hook does not invalidate on bid placement; balance shown may lag server state until next mount / window refocus. Wire your own invalidation (or layer a realtime push hook) if your UI is trading-critical.

useNetworkQuality

Subscribes to client.network (passive monitor in @taphubhq/sdk-core) and re-renders only when the committed network level OR backend health changes.

import { useNetworkQuality } from '@taphubhq/sdk-react';

function NetworkBanner() {
  const { network, backend } = useNetworkQuality();
  if (network === 'offline') return <Banner>Offline</Banner>;
  if (backend === 'down') return <Banner>Servers having trouble</Banner>;
  if (network === 'poor') return <Banner>Weak connection</Banner>;
  return null;
}

Returns the full NetworkQuality shape (rtt, jitter, lossRate, mqttConnected, samplesInWindow, lastUpdated) — pick the fields you need. SSR-safe: the hook returns a deterministic snapshot during server rendering. The network (good/fair/poor/offline) and backend (ok/degraded/down) signals are independent — backend incidents do not pollute the network indicator.

sortOrders

Pure helper that sorts bids: pending-first, then by time2 descending, then by createdAt descending.

import { useOrders, sortOrders } from '@taphubhq/sdk-react';

const { orders } = useOrders({ filter: 'all' });
const sorted = sortOrders(orders);