Skip to content
You are reading the v2 docs, currently in beta.V1 docs
oRPC
Esc
navigateopen⌘Jpreview
On this page

Expo Adapter

Use oRPC in an Expo app over fetch or WebSocket, including streaming support, binary data, and the SDK versions each feature needs.

Expo is the supported way to run oRPC in a React Native app. It installs the browser APIs oRPC depends on, which bare React Native does not provide, so JSON calls, AsyncIteratorObject, and streamed downloads work in a plain Expo project with no polyfill and no Metro configuration. Some File and Blob cases need one extra package.

Two of those matter most:

  • Web Streams. oRPC extends TransformStream at module scope, so the global must exist before oRPC is imported. Expo installs ReadableStream, WritableStream, and TransformStream as native globals from SDK 53. Without them, import '@orpc/client' throws ReferenceError: TransformStream is not defined.
  • A streaming fetch. React Native’s built-in fetch is an XMLHttpRequest polyfill with no response.body, which rules out AsyncIteratorObject and streamed downloads. expo/fetch is native-backed and streams properly. From SDK 56 it replaces the global fetch on Android and iOS, so there is nothing to wire up.
import { RPCLink } from '@orpc/client/fetch'

export const link = new RPCLink({
  origin: process.env.EXPO_PUBLIC_SERVER_URL,
  url: '/rpc',
  headers: async ({ context }) => ({
    'x-api-key': context?.something ?? '',
  }),
})

Expo SDK 53 to 55

On these versions the global fetch is still React Native’s non-streaming polyfill, so pass expo/fetch to the link explicitly. Everything else stays the same.

import { fetch as expoFetch } from 'expo/fetch'
import { RPCLink } from '@orpc/client/fetch'

const link = new RPCLink({
  origin: process.env.EXPO_PUBLIC_SERVER_URL,
  url: '/rpc',
  fetch: (url, init) => expoFetch(url, init),
})

Expo inherits React Native’s WebSocket, which is backed by OkHttp on Android and SocketRocket on iOS and handles binary frames in both directions.

import { RPCLink } from '@orpc/client/websocket'

const link = new RPCLink({
  connect: () => {
    const ws = new WebSocket('ws://localhost:3000')
    ws.binaryType = 'arraybuffer'
    return ws
  },
})

Feature Support

oRPC treats binary data two different ways, and the difference decides what works on Expo:

  • Root level, where a File or Blob is the entire input or output. oRPC sends it as the raw body, with no multipart involved.
  • Nested, where a File or Blob sits inside an object or array. oRPC packs it into a FormData, which travels as multipart/form-data.

Expo never parses multipart, and the WebSocket link cannot even produce it, which is why every nested row below fails somewhere. Everything else depends on whether you install the Blob polyfill.

Feature Fetch Fetch + polyfill WebSocket WebSocket + polyfill
AsyncIteratorObject download Yes Yes Yes Yes
AsyncIteratorObject upload Buffered Buffered Yes Yes
ReadableStream<Uint8Array> download Yes Yes Yes Yes
ReadableStream upload Buffered Buffered Yes Yes
Root-level File or Blob download No Yes No Yes
Root-level File or Blob upload Yes Yes No Yes
Nested File or Blob download No No No No
Nested File or Blob upload No Yes No No

Two things are worth knowing beyond the table:

  • expo/fetch buffers every upload. It accepts a streamed request body, whether that is an AsyncIteratorObject or a ReadableStream, but drains it into one buffer before the request starts. Nothing reaches the server until the stream finishes, and the whole payload sits in memory meanwhile. Downloads stream properly. The WebSocket link has no such limit: each chunk is its own frame, sent immediately, so uploads genuinely stream. It is the one place the WebSocket link beats fetch.
  • The WebSocket link handles nested binary worse. It runs FormData through the global Response in both directions, and Expo leaves that as React Native’s implementation, which can neither read a FormData body as a blob nor parse a multipart one. The fetch link only has the decode half of that problem.

Blob Polyfill

React Native’s Blob has no arrayBuffer(), text(), bytes(), or stream(), and new Blob([arrayBuffer]) throws, so oRPC cannot read bytes back out of a downloaded blob. expo-blob is Expo’s spec-compliant implementation. It fixes every root-level case in the table above, plus nested uploads on the fetch link.

npx expo install expo-blob
pnpm dlx expo install expo-blob
yarn dlx expo install expo-blob
bunx expo install expo-blob

It exports only Blob, so declare File yourself and install both as globals from your entry file, before any oRPC import:

import { Blob, type BlobPart } from 'expo-blob'

class File extends Blob {
  name: string
  lastModified: number
  webkitRelativePath = ''

  constructor(fileBits: BlobPart[] | Iterable<BlobPart>, fileName: string, options?: FilePropertyBag) {
    super(fileBits, options)

    this.name = String(fileName)
    this.lastModified = options?.lastModified ?? Date.now()
  }

  override toString(): string {
    return '[object File]'
  }
}

Object.assign(globalThis, { Blob, File })

Why nested binary fails

A nested File or Blob is packed into a FormData and sent as multipart/form-data. Reading it back needs Response.formData(), and no Response on Expo can do that. Both expo/fetch and React Native handle only the simpler application/x-www-form-urlencoded format.

Writing multipart works, but only on the fetch link, where expo/fetch does the encoding. The WebSocket link uses Response to write it too, so that link fails in both directions.

Link Nested upload Nested download
Fetch Works with the Blob polyfill Fails
WebSocket Fails Fails

The easy fix is to avoid multipart. Keep files at the root of a procedure’s input or output instead of inside an object. Or extend the RPC JSON Serializer to carry binary as base64.

The full fix is a Response that handles multipart. For the WebSocket link, replace globalThis.Response with one that can both write and read it. For the fetch link only reading is missing, so extend the response that expo/fetch returns.

Bare React Native

oRPC does not support bare React Native out of the box. Without Expo you get no Web Streams globals, so import '@orpc/client' throws before your code runs, and no streaming fetch. You can get close by adding web-streams-polyfill and loading it from your entry file before any oRPC import, but AsyncIteratorObject and streamed downloads will still not work over the built-in fetch. React Native 0.78 is also a hard floor: oRPC uses a class static initialization block, Metro does not transpile it, and older Hermes cannot parse it. See Requirements for the full list of APIs oRPC needs.

Last updated on August 24, 2026

Was this page helpful?