React Native and Expo quickstart
Build an iOS and Android screen that reads a public BTC REST price and subscribes to WebSocket prices with @siebly/hyperliquid-api. No API key is needed. Prices are BTC mid 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 hyperliquid-expo-quickstart
cd hyperliquid-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. No additional SDK polyfill configuration is needed for this recipe. See Runtime compatibility for the tested OS, Hermes and Expo Go versions.
Project files
package.json
{
"name": "hyperliquid-expo-quickstart",
"private": true,
"type": "module",
"scripts": {
"start": "expo start",
"ios": "expo start --ios",
"android": "expo start --android"
},
"dependencies": {
"@siebly/hyperliquid-api": "latest",
"expo": "58.0.23",
"react": "19.3.0",
"react-native": "0.88.1"
},
"main": "index.js"
}market.js
export const endpoints = {"rest": "https://api.hyperliquid.xyz/info", "websocket": "wss://api.hyperliquid.xyz/ws"};
export function client(sdk) { return new sdk.RestClient({}, {timeout:15000}); }
export async function read(client) { return (await client.getAllMids()).BTC; }
export function stream(sdk, logger) { return new sdk.WebsocketClient({}, logger); }
export const event = 'message';
export function value(message) { return message.channel === 'allMids' ? message.data?.mids?.BTC : undefined; }
export async function subscribe(ws) { await ws.subscribe({ type: 'allMids' }); }price.js
import * as sdk from '@siebly/hyperliquid-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);
};
}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": "Hyperliquid BTC price",
"slug": "hyperliquid-btc-price",
"version": "1.0.0",
"platforms": [
"ios",
"android"
]
}
}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
- If an SDK import fails, run
npm installin the starter directory and check that the installed package matches the import inprice.js. - 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.