Skip to main content

@stewmore/expo-ai-react

React hooks for the runtime — ai/react-style ergonomics over the pure-TS core. Use them for streaming chat, partial-object streaming, and capability-gated UI without writing your own AbortController/isMounted plumbing.

npm install @stewmore/expo-ai-react

The package peer-depends on react only (no React Native import), so the hooks work in any React renderer. Each hook owns an AbortController: stop() and unmount both cancel cleanly, and a cancellation is never surfaced as an error.

useGenerate

One-shot or token-streamed text generation.

import { useGenerate } from '@stewmore/expo-ai-react';

function Composer() {
const { stream, text, isLoading, error, stop } = useGenerate();

return (
<>
<Button
title="Write"
onPress={() => stream({ prompt: 'Write a haiku about the sea.', fallback: 'cloud' })}
/>
{isLoading ? <Button title="Stop" onPress={stop} /> : null}
<Text>{text}</Text>
{error ? <Text>{error.message}</Text> : null}
</>
);
}

{ generate, stream, text, result, isLoading, error, stop, reset }generate is one-shot; stream accumulates text as tokens arrive. Both resolve the final GenerateResult (with provider + privacy).

useChat

A streaming chat transcript over a cross-platform session.

import { useChat } from '@stewmore/expo-ai-react';

function Chat() {
const { messages, input, setInput, append, isLoading, stop } = useChat({ fallback: 'cloud' });

return (
<>
{messages.map((m) => (
<Text key={m.id}>
{m.role}: {m.content}
</Text>
))}
<TextInput value={input} onChangeText={setInput} />
<Button title="Send" onPress={() => append()} disabled={isLoading} />
</>
);
}

{ messages, input, setInput, append, isLoading, error, stop, reset }. The session is created lazily on the first append and disposed on unmount; append(content?) defaults to the current input.

useObject

Partial-object streaming via streamObjectobject fills in as tokens arrive, then becomes the validated final value.

import { useObject } from '@stewmore/expo-ai-react';

const recipeSchema = {
type: 'object',
properties: {
title: { type: 'string' },
steps: { type: 'array', items: { type: 'string' } },
},
required: ['title', 'steps'],
} as const;

function Recipe() {
const { submit, object, isLoading } = useObject<{ title: string; steps: string[] }>();

return (
<>
<Button
title="Generate"
onPress={() => submit({ prompt: 'A pasta recipe.', schema: recipeSchema })}
/>
<Text>{object?.title}</Text>
{object?.steps?.map((s, i) => (
<Text key={i}>{s}</Text>
))}
</>
);
}

{ submit, object, isLoading, error, stop, reset }. object is a DeepPartial<T> while streaming and the validated T once complete.

useCapabilities

What the runtime can do on this device — resolved on mount, re-queryable.

import { useCapabilities } from '@stewmore/expo-ai-react';

function Gate() {
const { capabilities, loading, refresh } = useCapabilities();
if (loading) return <Spinner />;
if (!capabilities?.supportsStreaming) return <Text>Streaming unavailable on this device.</Text>;
return <Composer />;
}

{ capabilities, availability, providers, loading, error, refresh }. Call refresh() after the user enables Apple Intelligence or finishes a model download. See capability detection.