React Native

Learn how to use Jazz from React Native and Expo apps.

React Native and Expo are supported as an alpha through jazz-tools/react-native and the direct jazz-rn native dependency. Use a matching development or release build: Expo Go is unsupported, and so is Expo web (it renders a blank page). For the web, build a separate app with the web client.

Setup

React Native and Expo use the same session API with platform-specific runtime and account storage adapters:

import { JazzSessionProvider } from "jazz-tools/expo";
​
function App() {
return (
<JazzSessionProvider config={{ serverUrl, initial: "local-first" }}>
<TodoApp />
</JazzSessionProvider>
);
}

Install

Use Expo SDK 54 or later (SDK 58 included). Install Jazz with the Expo modules it uses for crypto and the OS-protected account store, plus babel-preset-expo:

Terminal
npm install jazz-tools jazz-rn
npx expo install expo-crypto expo-secure-store babel-preset-expo

npx expo install picks the babel-preset-expo version that matches your SDK.

jazz-rn ships Jazz's engine prebuilt for Android and iOS, so you don't need a Rust toolchain, but both platforms' native libraries are always installed: about 500 MB in node_modules. Building the app needs the usual native toolchains: for Android, the Android SDK (min SDK 24), NDK 27.1.12297006 and CMake 3.22.1 or later, which Android Studio's SDK Manager installs; for iOS, Xcode and CocoaPods.

Jazz doesn't need jazz-tools/expo/polyfills. It only installs a global ReadableStream, which Hermes lacks, for code of your own that uses web streams; streaming writes such as insertStreaming also take an async iterable.

Add the jazz-rn config plugin to app.json:

app.json
{
"expo": {
"plugins": ["jazz-rn"]
}
}

Let Babel transform import.meta, which Jazz uses:

babel.config.js
module.exports = function (api) {
api.cache(true);
return {
presets: [["babel-preset-expo", { unstable_transformImportMeta: true }]],
};
};

Jazz needs the New Architecture, which is the default from SDK 54. Then build the native app with npx expo prebuild and npx expo run:android or npx expo run:ios, or with EAS Build. Expo Go can't load Jazz's native module. After changing the Babel config, start Metro with npx expo start --clear.

To reach a backend on your computer, the Android emulator uses http://10.0.2.2:<port> instead of localhost, and Android blocks plain http:// unless you allow cleartext traffic for development.

A bare React Native host uses the same direct dependency and New Architecture, with an OS-protected account store. The canonical Expo scaffold shows the complete lifecycle and platform configuration. The alpha has not yet proven two physical JSI runtimes attached to one relay.

Loading and errors

JazzSessionProvider renders fallback until the session is ready. Inside it, useJazzSession() gives the session's status ("transitioning", "signed-out", "error", "ready" or "closed"), its error and retry():

import { JazzSessionProvider, useJazzSession } from "jazz-tools/expo";
​
function Startup() {
const { status, error, retry } = useJazzSession();
if (status === "error") return <Button title={`${error?.message}: retry`} onPress={retry} />;
return <ActivityIndicator />;
}
​
<JazzSessionProvider config={{ serverUrl, initial: "local-first" }} fallback={<Startup />}>
<TodoApp />
</JazzSessionProvider>;

Which entry exports what

Import from jazz-tools/expo in an Expo app and jazz-tools/react-native in a bare one. Both export the providers, useJazzSession, useJazzAuth, useDb, useSession, useAll, useOne and createAccountManager. useSyncStatus and useAccountState are only in jazz-tools/react-native for now; an Expo app imports those two from there.

Note

serverUrl is your backend's Jazz endpoint (where createJazz is mounted); the app names no app id. For Jazz on the device only, pass serverUrl: null: no backend, one local-first account on this device.

Some bundlers and runtimes cannot automatically resolve Jazz's Wasm and worker assets. See Wasm loading.

Sign in through your backend

An app has no cookie for your backend, so it hands Jazz its own token instead. Pass bearer, a function that returns the token your app holds for its backend (or null when signed out), next to serverUrl in createAccountManager from jazz-tools/react-native (or jazz-tools/expo). Jazz sends it as Authorization: Bearer … when it signs in and on the device's sync connection, so your backend's authenticate reads that header instead of a cookie.

After your own sign-in, call loginWithBackend() for the user's own account, or linkWithBackend() to keep a local-first guest's data under the new user, both from useJazzAuth().sessionActions; useJazzAuth().logout() signs out. Linking a user who already has an account fails with an AccountAuthError whose code is link_refused; sign them in with loginWithBackend() instead.

To reach a development backend over plain http:// on Android, allow cleartext traffic, for example with expo-build-properties (android.usesCleartextTraffic: true) in a development build only.

Testing without a device

Test your schema, permissions and backend logic in Node against your app's backend, as in Testing. A Node client stands in for the device: give createJazzClient from jazz-tools/client a store to keep its account in (the testing page shows an in-memory one), since outside the browser and the app there is no default. Use bearer: () => testJazz.bearer(userId) to sign in the way the app does. The React Native UI itself needs an emulator or a device.

Coming next

  • Expo vs bare React Native setup, step by step
  • Today React Native clients run the local-first engine on the device; a thin client follows the web thin client