Skip to content

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.

sh
mkdir hyperliquid-expo-quickstart
cd hyperliquid-expo-quickstart

Copy the files below, or extract the downloaded archive into this directory (adjust the download path if needed):

sh
tar -xzf ~/Downloads/expo.tar.gz

Install the dependencies after saving or extracting the files:

sh
npm install

The 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 ​

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 ​

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 ​

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 ​

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 ​

js
import { registerRootComponent } from 'expo';
import App from './App';
registerRootComponent(App);

app.json ​

json
{
  "expo": {
    "name": "Hyperliquid BTC price",
    "slug": "hyperliquid-btc-price",
    "version": "1.0.0",
    "platforms": [
      "ios",
      "android"
    ]
  }
}

Run on iOS or Android ​

sh
npm start

Press 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 install in the starter directory and check that the installed package matches the import in price.js.
  • If Metro keeps an old resolution error, stop it and run npx expo start --clear from 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.js from the simulator or emulator. Exchange regional restrictions apply there too.

Built by Siebly. Open source SDKs for exchange APIs.