React Native and Expo quickstart
Build an iOS and Android screen that reads a public BTC REST price and subscribes to WebSocket prices with binance. No API key is needed. Prices are BTCUSDT last traded prices.
Create the project
Use Node.js with npm and an iOS simulator or Android emulator with a matching Expo Go installation. iOS simulators require macOS and Xcode; Android needs a running virtual device created with Android Studio or the Android SDK command-line tools.
Download the complete Expo starter, or create an empty directory and copy every file below. This is a plain Expo app with index.js as its entry point.
mkdir binance-expo-quickstart
cd binance-expo-quickstartCopy the files below, or extract the downloaded archive into this directory (adjust the download path if needed):
tar -xzf ~/Downloads/expo.tar.gzInstall the dependencies after saving or extracting the files:
npm installThe SDK installs from latest. The starter uses Expo 58.0.23, React Native 0.88.1 and React 19.3.0. Keep both Metro configuration files and the five polyfill dependencies. They are the same resolver configuration used in the native checks. See Runtime compatibility for the tested OS, Hermes and Expo Go versions.
Project files
package.json
{
"name": "binance-expo-quickstart",
"private": true,
"type": "module",
"scripts": {
"start": "expo start",
"ios": "expo start --ios",
"android": "expo start --android"
},
"dependencies": {
"binance": "latest",
"expo": "58.0.23",
"react": "19.3.0",
"react-native": "0.88.1",
"buffer": "6.0.3",
"events": "3.3.0",
"https-browserify": "1.0.0",
"stream-http": "3.2.0",
"url": "0.11.4"
},
"main": "index.js"
}market.js
export const endpoints = {"rest": "https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT", "websocket": "wss://stream.binance.com:9443"};
export function client(sdk) { return new sdk.MainClient({}, {timeout:15000}); }
export async function read(client) {
const response = await client.getSymbolPriceTicker({ symbol: 'BTCUSDT' });
if (Array.isArray(response)) throw new Error('Expected one ticker');
return response.price;
}
export function stream(sdk, logger) { return new sdk.WebsocketClient({}, logger); }
export const event = 'message';
export function value(message) { return message.e === '24hrTicker' ? message.c : undefined; }
export async function subscribe(ws) { await ws.subscribeSpotSymbol24hrTicker('btcusdt'); }price.js
import * as sdk from 'binance';
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);
};
}App.js
import React, { useEffect, useState } from 'react';
import { Button, SafeAreaView, Text, View } from 'react-native';
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.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 <View style={{ gap: 12, paddingVertical: 24 }}>
<Text>BTC REST price: {rest}</Text>
<Text>BTC WebSocket price: {live}</Text>
<Text accessibilityRole="alert">{error}</Text>
</View>;
}
export default function App() {
const [running, setRunning] = useState(true);
return <SafeAreaView style={{ flex: 1 }}><View style={{ padding: 24 }}>
<Text style={{ fontSize: 24 }}>BTC price</Text>
{running ? <Prices /> : <Text style={{ paddingVertical: 24 }}>Stream stopped. WebSocket closed.</Text>}
<Button title={running ? 'Stop stream' : 'Start stream'} onPress={() => setRunning(value => !value)} />
</View></SafeAreaView>;
}index.js
import { registerRootComponent } from 'expo';
import App from './App';
registerRootComponent(App);app.json
{
"expo": {
"name": "Binance BTC price",
"slug": "binance-btc-price",
"version": "1.0.0",
"platforms": [
"ios",
"android"
]
}
}siebly-sdk.metro.cjs
const path = require('node:path');
module.exports = function configureSdkMetro(config, projectRoot) {
const resolve = name => require.resolve(name, {paths:[projectRoot]});
const aliases = {
https: resolve('https-browserify'),
http: resolve('stream-http'),
url: resolve('url/'),
};
const previous = config.resolver.resolveRequest;
const binanceNodeSupport = path.join(projectRoot,'node_modules/binance/lib/util/node-support');
config.resolver.resolveRequest = (context, name, platform) => {
const resolveRequest = previous || context.resolveRequest;
if (aliases[name]) return {type:'sourceFile',filePath:aliases[name]};
if (name.startsWith('.') && path.resolve(path.dirname(context.originModulePath),name).replace(/\.js$/,'') === binanceNodeSupport) {
return {type:'sourceFile',filePath:path.join(projectRoot,'node_modules/binance/lib/util/browser-support.js')};
}
return resolveRequest(context,name,platform);
};
return config;
};metro.config.cjs
const { getDefaultConfig } = require('expo/metro-config');
module.exports = require('./siebly-sdk.metro.cjs')(getDefaultConfig(__dirname), __dirname);Run on iOS or Android
npm startPress i to open the iOS simulator or a to open the Android emulator. Keep Expo Go in the foreground. The screen shows BTC REST price and BTC WebSocket price, each followed by a positive price such as 65000.5.
Tap Stop stream. The price component unmounts, its effect cleanup calls closeAll(), and the screen shows Stream stopped. WebSocket closed. Tap Start stream to create a new subscription. Late REST responses are ignored after unmount.
The accepted runtime checks cover public REST, WebSocket updates and cleanup in Expo Go on an iOS simulator and Android emulator. Physical devices, release builds, background recovery and authenticated trading need separate validation for your app.
Troubleshooting
- An unresolved
events,https,http,urlorbufferimport means a dependency or Metro helper is missing. Install all dependencies and keepmetro.config.cjsbesidepackage.json. - If Metro keeps an old resolution error, stop it and run
npx expo start --clearfrom the starter directory. - If Expo Go rejects the project version, use the Expo Go release that supports the starter's Expo SDK. Check the runtime report for the version used in the recorded checks.
- If price fields stay at Loading or Connecting, allow the exchange HTTPS and WSS endpoints shown in
market.jsfrom the simulator or emulator. Exchange regional restrictions apply there too.