Skip to content

Instantly share code, notes, and snippets.

@aretrace
Last active February 20, 2025 17:21
Show Gist options
  • Save aretrace/bcb0777c2cfd2b0b1d9dcfb805fe2838 to your computer and use it in GitHub Desktop.
Save aretrace/bcb0777c2cfd2b0b1d9dcfb805fe2838 to your computer and use it in GitHub Desktop.

How to stream in React Native ⼮

Prelude ䷿

  1. npx create-expo-app@latest
  2. npm run reset-project
  3. rm -rf node_modules
  4. npm i react-native-fast-encoder web-streams-polyfill @stardazed/streams-text-encoding react-native-fetch-api
  5. (optional) echo "BROWSER=none # prevents browser from auto opening" >> .env

Main Event 🎪

  1. In the root of the project directory, create a index.ts with the following content:
// 📍 ./index.ts

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import { Platform } from "react-native";
import TextEncoder from "react-native-fast-encoder";

if (Platform.OS !== "web") {
  const setupPolyfills = async () => {
    const { polyfillGlobal } = await import(
      "react-native/Libraries/Utilities/PolyfillFunctions"
    );
    const { ReadableStream, TransformStream } = await import(
      "web-streams-polyfill/dist/ponyfill"
    );
    const { TextEncoderStream, TextDecoderStream } = await import(
      "@stardazed/streams-text-encoding"
    );
    const { fetch, Headers, Request, Response } = await import(
      "react-native-fetch-api"
    );

    polyfillGlobal("TextDecoder", () => TextEncoder);
    polyfillGlobal("ReadableStream", () => ReadableStream);
    polyfillGlobal("TransformStream", () => TransformStream);
    polyfillGlobal("TextEncoderStream", () => TextEncoderStream);
    polyfillGlobal("TextDecoderStream", () => TextDecoderStream);
    polyfillGlobal(
      "fetch",
      () =>
        (...args) =>
          fetch(args[0], { ...args[1], reactNative: { textStreaming: true } })
    );
    polyfillGlobal("Headers", () => Headers);
    polyfillGlobal("Request", () => Request);
    polyfillGlobal("Response", () => Response);
  };

  setupPolyfills();
}

import "expo-router/entry";

declare global {
  interface RequestInit {
    /**
     * @description Polyfilled to enable text ReadableStream for React Native:
     * @link https://github.com/facebook/react-native/issues/27741#issuecomment-2362901032
     */
    reactNative?: {
      textStreaming: boolean;
    };
  }
}
  1. In the project package.json, replace the main top level property value with the following:
{
  ...
- "main": "expo-router/entry",
+ "main": "./index",
  ...
}

   3. Test Code 🧑‍💻

      𓃾.  npm i openai

      a.  Modify app.json in the following way:

// 📍 ./app.json

{
  "web": {
    "bundler": "metro",
-   "output": "static"
+   "output": "server"
  }
}

      b. Create ai+api.ts with the following:

// 📍 ./app/ai+api.ts

import { OpenAI } from "openai";

const client = new OpenAI({
  // baseURL: "...",
  apiKey: "...",
});

async function getTextStream() {
  return await client.chat.completions.create({
    model: "...",
    messages: [{ role: "user", content: "How many r's are there in STaR?" }],
    stream: true,
  });
}

export async function GET(request: Request) {
  const stream = await getTextStream();

  const textStream = new ReadableStream({
    async start(controller) {
      try {
        for await (const chunk of stream) {
          const content = chunk.choices[0]?.delta?.content || "";
          controller.enqueue(content);
        }
      } catch (e) {
        controller.error(e);
      } finally {
        controller.close();
      }
    },
  }).pipeThrough(new TextEncoderStream());

  return new Response(textStream, {
    headers: {
      "Content-Type": "text/plain; charset=utf-8",
    },
  });
}

      c. Add the following to index.ts:

// 📍 ./app/index.tsx

+ import { useCallback, useState } from "react";
import { Text, View } from "react-native";

export default function Index() {
+  const [response, setResponse] = useState("");
+  const [isStreaming, setIsStreaming] = useState(false);
+
+  const handlePress = useCallback(async () => {
+    setResponse("");
+    setIsStreaming(true);
+
+    const response = await fetch("http://localhost:8081/ai");
+    const reader = response.body?.pipeThrough(new TextDecoderStream()).getReader();
+
+    while (true) {
+      try {
+       const { value, done } = await reader!.read();
+        if (done) break;
+        setResponse((prev) => prev + value);
+      } catch (e) {
+        console.error(`Error reading response: ${e}`);
+        setIsStreaming(false);
+        break;
+      } finally {
+        setIsStreaming(false);
+      }
+    }
+  }, []);
+
  return (
    <View
      style={{
        flex: 1,
        justifyContent: "center",
        alignItems: "center",
      }}
    >
-     <Text>Edit app/index.tsx to edit this screen.</Text>
+     <Pressable onPress={handlePress} disabled={isStreaming}>
+       <Text>Press me to stream!{"\n"}</Text>
+     </Pressable>
+     <Text>Streamed response:{"\n"}</Text>
+     <Text>{response}</Text>
+   </View>
  );
}

      c. npx expo run:<ios|android>

  1. Profit 💰💰💰
Special thanks to:

please voice your support for native streaming at facebook/react-native#27741

@MannyGozzi
Copy link

Hey thanks for the solution! Does this also require enabling the
unstable_enablePackageExports: true, flag in metro.config.js?
I still get the error that ai/react cannot be resolved and adding this flag gives an import.meta error.

@hollanderbart
Copy link

Are all these polyfills still needed with the latest expo/fetch implementation in SDK 52? I don't think it's needed anymore because it should be supported now.

@markwitt1
Copy link

Any update on this? @hollanderbart

@hollanderbart
Copy link

No polyfills are needed when using the latest expo 52 with expo/fetch

@nordwestt
Copy link

Tried upgrading to expo 52 and passing expo/fetch, but I get an AI_APICallError that I haven't been able to solve
@hollanderbart did you personally get this working? if so, could you share the repo or some example code?
All help is appreciated !

@tanishqkancharla
Copy link

@hollanderbart , expo/fetch doesn't implement TextEncoderStream and TextDecoderStream, so you still need those.

@tanishqkancharla
Copy link

For those using expo/fetch on SDK 52, this got streaming working for me:

import {
	TextDecoderStream,
	TextEncoderStream,
} from "@stardazed/streams-text-encoding";
import { fetch } from "expo/fetch";

export const originalFetch = global.fetch;
global.fetch = fetch as any;

global.TextEncoderStream = TextEncoderStream;
global.TextDecoderStream = TextDecoderStream;

@hollanderbart
Copy link

hollanderbart commented Feb 17, 2025

TextEncoderStream isn't implemented in Expo indeed. But TextEncoder is part of Expo now, which also works.
TextEncoderStream is a newer implementation to use a streaming variant of TextEncoder, but you can also add steaming support without it.

@ajp8164
Copy link

ajp8164 commented Feb 20, 2025

Having this issue with react-native-fast-encoder. Anyone else see this?

maksimlya/react-native-fast-encoder#4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment