Guides

Adding an AI agent to a React Native app

The real integration: five calls across the bridge, what your JavaScript owns, what the native side draws, and the week-two work nobody puts in a quickstart.

VVoqal · Engineering9 min readPart of Integration

About this article

React Native teams have two ways to put an agent in an app. Render the conversation in React, which means owning a message list, a streaming renderer, a widget set and an audio session in JavaScript. Or hand the whole surface to a native module and keep five function calls.

This post is about the second approach, using our own bridge as the worked example. Two things to know before you start: the package is iOS-only today, and it is in private beta, so the install below will not resolve until your npm account has access. The Android and Flutter SDKs are public; this one is not yet.

forwardedHeaders is the only one called repeatedly. The other four happen once at app start, or once per tap.

Why the UI stays native#

A conversation surface is a hard thing to render well in React Native. It streams, it scrolls while it streams, it plays audio, it has to be interruptible mid-sentence, and it draws a different shape for every kind of answer. Building that in JavaScript means the bridge carries traffic on every token.

The New Architecture has made that far less painful. React Native’s architecture overview explains that it “removes the asynchronous bridge between JavaScript and native and replaces it with JavaScript Interface (JSI)”, letting JavaScript hold a reference to a C++ object and call methods without serialisation cost. The New Architecture has been the default since React Native 0.76.

Even so, the cheapest version of this feature is the one where the conversation never crosses into JavaScript at all. The native module owns the microphone, the streaming, the widgets and the layout. Your JavaScript owns when to open it and who the user is.

The install#

bash
# Requires beta access: the package is private.
npm install @voqal/react-native
cd ios && pod install

Add a microphone usage string to ios/<YourApp>/Info.plist, or the first tap will fail silently on device:

xml
<key>NSMicrophoneUsageDescription</key>
<string>Voqal uses the microphone for voice conversations.</string>

The deployment target is iOS 16. That floor exists because the widget layer uses layout APIs introduced in iOS 16, and back-porting it is real work rather than a target flip.

Configure once#

js
import Voqal from '@voqal/react-native';

Voqal.setup({
  apiKey: 'pk_live_…',            // publishable, sent as X-Voqal-Key
  requestId: 'prod-yourapp',      // the "prod-" or "stg-" prefix picks the environment
  theme: { accent: '#2d5bff', appearance: 'auto' },
});

The key is publishable and belongs in your client bundle. The request id prefix is how one build talks to staging and another talks to production without a second key.

Keep credentials fresh#

js
// Call at start, and again whenever your auth layer rotates the token.
Voqal.setCredentials(userToken, JSON.stringify({ country_code: 'EGY' }));

This is the call teams get wrong first. The token you pass is your user’s own credential, and the agent acts with exactly the permissions that token carries. It cannot read or write anything the user could not reach by tapping through the app by hand. If your tokens expire hourly, call this again on refresh rather than at app start only.

If your backend needs headers of its own on every turn, pass a function rather than a value so it is evaluated fresh:

js
Voqal.setup({
  apiKey: 'pk_live_…',
  requestId: 'prod-yourapp',
  forwardedHeaders: () => ({
    Authorization: `Bearer ${getAccessToken()}`,
    'X-Tenant-Id': currentTenantId(),
  }),
});

Present it from your own button#

js
Voqal.prewarm();   // optional, at app start

<Pressable
  onPress={() => Voqal.present()}
  accessibilityLabel="Ask"
  style={styles.launcher}
/>

The bridge ships no launcher component on purpose. A bundled floating button lands in the wrong place in most apps, overlaps a tab bar in some, and fights the safe area in others. Placement is a design decision, so it stays with the designer.

prewarm opens the connection to the runtime and primes the model’s prompt cache before anyone asks anything. It is worth calling at launch: a cold first turn can run several times longer than a warm one, and almost all of that is the round trip to the backend rather than anything in your app.

What you write, and what you do not#

Only the top row is yours, and only one item in it is real engineering. The tool definitions are the project.

The bottom row is the part that looks like a quickstart and is actually a year of work if you build it. The top row is short, and one entry in it dominates everything else: deciding which of your operations the agent can call, what they are named, and what their errors say. Designing the tool surface is where a good integration is won or lost.

Worth knowing before you design against it: the answers the agent returns are drawn from a fixed set of shapes rather than free-form markup, so what you can express is a closed list you can read in advance. On the native iOS side the same SDK is driven through a five-method delegate, which the SDK-surface post walks through if you are also shipping a native target.

Adding another language#

Language is a configuration value here rather than a second code path. The agent replies in whatever the user speaks, and the widget layer mirrors itself for right-to-left scripts without you laying out a second screen.

The part that is not free is knowing whether recognition holds up for the varieties your users actually speak. Published zero-shot results for Arabic put the same model near 15% word error on the written standard and near 79% on one regional variety, which is the difference between a working feature and a broken one. Testing a speech stack per variety rather than per language sets out how to measure that before you ship.

If you would rather render it in React#

Some teams want the conversation inside their own React tree, usually because the assistant is meant to live inline in a screen instead of over it. That is a legitimate requirement and it changes which vendor fits.

ElevenLabs publishes a React Native library whose API matches their web React SDK, with hooks for session state, microphone control and client-side tools. It carries two constraints worth reading before you commit: it “requires development builds and cannot be used with Expo Go”, and it wants React Native 0.79 or later unless you configure Metro to resolve the exports field yourself. If your agent only ever speaks, and everything on screen is a transcript, that is a shorter route than a native module.

What you take on in exchange is the interface layer. A React-rendered agent gives you a text stream and leaves you to decide what a balance, a list of twelve transactions or a confirmation looks like. That decision arrives once per answer shape, and it does not stop arriving.

Testing it before anyone else sees it#

Run the same set of spoken questions at the agent repeatedly rather than once, and record which operation it called each time. An agent that picks correctly eight times out of ten is not eighty per cent working; it is a feature that fails for one user in five, and on a payment flow that is the only number anyone will remember.

Do the recording on a device, in the acoustic conditions your users are in. Simulator audio is clean in a way real rooms are not, and endpointing behaviour in particular looks better there than it deserves to.

What this actually costs you#

The estimates stop at the rule on purpose. Everything below it is a product decision, and product decisions do not speed up because the SDK is good.

An afternoon gets you a working spoken turn against one read-only operation, and what the rest of the first day gets you is the same story told as a schedule. That milestone is worth having and worth being sceptical of. Nordic APIs, writing about time to first call, puts the caveat plainly: “Making a first call is very different from someone becoming an active user.”

The work after that is naming your tools, writing errors a model can recover from, deciding which operations may write, and designing what the confirm card shows before money moves. None of it is React Native work. All of it decides whether the feature survives real users. The five layers underneath a shipped agent covers the stack this sits inside, one layer at a time.

Where the launcher goes#

Placement is the decision that most affects whether anyone uses the feature, and it is entirely yours. Three patterns cover almost every app.

A floating button above the tab bar works when the assistant is a general entry point and the app has no other floating control. Respect the safe area, and give it a resting position that does not sit on top of a primary action on the busiest screen.

A microphone inside your existing search field works when the assistant mostly answers questions about things the user is already looking for. It inherits the affordance people already understand, and it costs you nothing in screen real estate.

A contextual entry point on a specific screen works when the agent’s best operations are about that screen. A merchant looking at a transactions list is more likely to ask about transactions, and an assistant offered there needs no explanation.

What does not work is a menu item three taps deep. If the assistant is worth building, it is worth one tap from somewhere a user already goes.

What to log#

The SDK reports a completed turn and a failure through the two callbacks, and both belong in your own observability rather than only in ours. Log the turn identifier, which operation was called, whether it was a read or a write, and how long the whole turn took from tap to answer.

Do not log the transcript by default. It is user speech and it belongs to the user. If you need transcripts to improve the tool surface, collect them behind an explicit setting, keep them for a stated period, and say so where a user can read it.

Things that will bite you#

The microphone permission string is easy to forget and fails quietly in the simulator, where the mic is often stubbed. Test the first tap on a device.

Calling setCredentials once at app start works until a token expires. Wire it to your refresh path instead.

A present() call inside a modal you already presented will do nothing useful. Dismiss your own modal first.

Metro can fail to resolve the package on older React Native versions that do not read the exports field in package.json. React Native 0.79 and later handle it; below that, configure Metro explicitly.

And if you ship an Expo app, check which flavour you are on before you plan any of this. Native modules of this kind need a development build, so a project living entirely in Expo Go will have to move first, and that migration is its own piece of work rather than a configuration flag.

Sources#

Filed underIntegrationReact NativeAgentsVoice

Next

The quickstart wires the SDK into an app and runs one real turn against your own backend.

Open the React Native quickstart

The rest of Integration

Open the cluster

iOS, Android, Flutter, React Native and web, from the first install to the first real turn against your own backend.

Elsewhere on the map