npx create-expo-app@latest
npm run reset-project
rm -rf node_modules
npm i react-native-fast-encoder web-streams-polyfill @stardazed/streams-text-encoding react-native-fetch-api
- (optional)
echo "BROWSER=none # prevents browser from auto opening" >> .env
- 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;
};
}
}
- In the project
package.json
, replace themain
top level property value with the following:
{
...
- "main": "expo-router/entry",
+ "main": "./index",
...
}
𓃾. 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>
- Profit 💰💰💰
- expo/expo#25122 (comment)
- facebook/react-native#27741 (comment)
- vercel/ai#655 (comment)
- MattiasBuelens/web-streams-polyfill#116 (comment)
please voice your support for native streaming at facebook/react-native#27741
Hey thanks for the solution! Does this also require enabling the
unstable_enablePackageExports: true,
flag inmetro.config.js
?I still get the error that
ai/react
cannot be resolved and adding this flag gives animport.meta
error.