React quickstart
Display public BTC REST and WebSocket prices from bybit-api in a React component. No API key is needed. Prices are BTCUSDT last traded prices. This is a React browser app built with Vite; the plain JavaScript Vite guide uses DOM elements directly.
Create the project
Use Node.js 24 with npm. Download the complete React starter, or copy every file below.
mkdir bybit-react-quickstart
cd bybit-react-quickstartCopy the files below, or extract the downloaded archive into this directory (adjust the download path if needed):
tar -xzf ~/Downloads/react.tar.gzInstall the dependencies after saving or extracting the files:
npm installThe SDK installs from latest. This project uses React 19.2.0, Vite 5.4.21 and the React plugin 4.7.0. Keep the supplied Vite config files: it resolves the events polyfill.
Project files
package.json
{
"name": "bybit-react-quickstart",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host 127.0.0.1",
"build": "vite build",
"preview": "vite preview --host 127.0.0.1"
},
"dependencies": {
"bybit-api": "latest",
"react": "19.2.0",
"react-dom": "19.2.0",
"events": "3.3.0"
},
"devDependencies": {
"vite": "5.4.21",
"@vitejs/plugin-react": "4.7.0"
}
}market.js
export const endpoints = {"rest": "https://api.bybit.com/v5/market/tickers?category=spot&symbol=BTCUSDT", "websocket": "wss://stream.bybit.com/v5/public/spot"};
export function client(sdk) { return new sdk.RestClientV5({}, {timeout:15000}); }
export async function read(client) {
const response = await client.getTickers({ category: 'spot', symbol: 'BTCUSDT' });
if (response.retCode !== 0) throw new Error(response.retMsg);
return response.result.list[0].lastPrice;
}
export function stream(sdk, logger) { return new sdk.WebsocketClient({}, logger); }
export const event = 'update';
export function value(message) { return message.topic === 'tickers.BTCUSDT' ? message.data?.lastPrice : undefined; }
export async function subscribe(ws) { await Promise.all(ws.subscribeV5('tickers.BTCUSDT', 'spot')); }price.js
import * as sdk from 'bybit-api';
import * as market from './market.js';
function positivePrice(value) {
if (!Number.isFinite(Number(value)) || Number(value) <= 0) {
throw new Error('The exchange did not return a positive BTC price.');
}
return String(value);
}
export async function readPrice() {
return positivePrice(await market.read(market.client(sdk)));
}
// Returns cleanup without waiting for subscription acknowledgement.
export function watchPrice(onPrice, onError) {
const ws = market.stream(sdk);
let stopped = false;
const receive = message => {
const value = market.value(message);
if (!stopped && value !== undefined) {
clearTimeout(deadline);
try { onPrice(positivePrice(value)); } catch (error) { fail(error); }
}
};
const fail = error => { if (!stopped) onError(error); };
const deadline = setTimeout(() => fail(new Error('No BTC update within 30 seconds. Check network access.')), 30000);
ws.on(market.event, receive);
ws.on('exception', fail);
// React may clean up an effect before its queued subscription starts.
Promise.resolve().then(async () => {
if (stopped) return;
try {
await market.subscribe(ws);
} finally {
// A connection may finish opening after cleanup was requested.
if (stopped) ws.closeAll();
}
}).catch(fail);
return () => {
if (stopped) return;
stopped = true;
clearTimeout(deadline);
ws.closeAll();
ws.removeListener(market.event, receive);
ws.removeListener('exception', fail);
};
}index.html
<!doctype html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><title>React BTC price</title></head>
<body><div id="root"></div><script type="module" src="/main.jsx"></script></body>
</html>App.jsx
import React, { useEffect, useState } from 'react';
import { readPrice, watchPrice } from './price.js';
function Prices() {
const [rest, setRest] = useState('Loading');
const [live, setLive] = useState('Connecting');
const [error, setError] = useState('');
useEffect(() => {
let mounted = true;
const fail = value => { if (mounted) setError(value instanceof Error ? value.message : String(value)); };
readPrice().then(price => { if (mounted) setRest(price); }).catch(fail);
const stop = watchPrice(price => { if (mounted) setLive(price); }, fail);
return () => { mounted = false; stop(); };
}, []);
return <section>
<p>BTC REST price: <output aria-label="REST price">{rest}</output></p>
<p>BTC WebSocket price: <output aria-label="WebSocket price">{live}</output></p>
<p role="alert">{error}</p>
</section>;
}
export default function App() {
const [running, setRunning] = useState(true);
return <main>
<h1>BTC price</h1>
{running ? <Prices /> : <p>Stream stopped. WebSocket closed.</p>}
<button type="button" onClick={() => setRunning(value => !value)}>{running ? 'Stop stream' : 'Start stream'}</button>
</main>;
}main.jsx
import React, { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
const root = createRoot(document.getElementById('root'));
root.render(<StrictMode><App /></StrictMode>);
const stop = () => root.unmount();
window.addEventListener('pagehide', stop, { once: true });
if (import.meta.hot) import.meta.hot.dispose(() => {
window.removeEventListener('pagehide', stop);
stop();
});vite.config.js
import react from '@vitejs/plugin-react';
import sdkConfig from './sdk.vite.config.js';
export default { ...sdkConfig, plugins: [...(sdkConfig.plugins || []), react()] };sdk.vite.config.js
import {fileURLToPath} from 'node:url';
export default {
resolve: {
alias: {
events: fileURLToPath(new URL('./node_modules/events/events.js', import.meta.url)),
},
},
};Run
npm run devOpen Vite's printed URL, normally http://localhost:5173/. Both price fields should display positive numbers such as 65000.5. Tap Stop stream to unmount the price component and close its WebSocket. Start stream mounts a new component and creates a fresh subscription.
The effect ignores late REST responses after unmount. Its cleanup closes the client on unmount and before a replacement effect. React StrictMode deliberately performs an extra setup and cleanup in development. The same cleanup handles it; the entry module also unmounts the root when the page closes or Vite replaces that module. See React's effect lifecycle.
Build and preview the production app:
npm run build
npm run previewTroubleshooting
- An
eventsresolution error means the dependency or Vite config is missing. Copy the config exactly and restart Vite. - A brief extra connection in development is expected under StrictMode. Repeated updates after Stop stream mean cleanup is missing from the component you copied.
- Keep the SDK import inside this browser app's helper, not a private-key signing flow. API secrets belong on a server. This example covers public market data.
- For network failures, inspect the browser's HTTPS and WSS requests to the endpoints in
market.js. Exchange CORS and regional restrictions apply. See Runtime compatibility for the underlying browser recipe's recorded evidence and WebSocket subscriptions for subscription details.