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
TransformStreamat module scope, so the global must exist before oRPC is imported. Expo installsReadableStream,WritableStream, andTransformStreamas native globals from SDK 53. Without them,import '@orpc/client'throwsReferenceError: TransformStream is not defined. - A streaming
fetch. React Native’s built-infetchis anXMLHttpRequestpolyfill with noresponse.body, which rules out AsyncIteratorObject and streamed downloads.expo/fetchis native-backed and streams properly. From SDK 56 it replaces the globalfetchon Android and iOS, so there is nothing to wire up.
Fetch Link
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),
})
WebSocket Link
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
FileorBlobis the entire input or output. oRPC sends it as the raw body, with no multipart involved. - Nested, where a
FileorBlobsits inside an object or array. oRPC packs it into aFormData, which travels asmultipart/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/fetchbuffers every upload. It accepts a streamed request body, whether that is anAsyncIteratorObjector aReadableStream, 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
FormDatathrough the globalResponsein both directions, and Expo leaves that as React Native’s implementation, which can neither read aFormDatabody 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-blobpnpm dlx expo install expo-blobyarn dlx expo install expo-blobbunx expo install expo-blobIt 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.