React Native SDK
Package: @loomup/react-native (SDK repository path: packages/react-native).
Thin React Native helpers on top of @loomup/client and @loomup/react: createNativeClient, AsyncStorage-backed session persistence, and re-exported hooks.
Install
# monorepo paths
npm install ../path/to/packages/client ../path/to/packages/react ../path/to/packages/react-native
npm install @react-native-async-storage/async-storage
# or after publish:
# npm install @loomup/client @loomup/react @loomup/react-native @react-native-async-storage/async-storage
Peer dependencies: react ≥ 18, react-native ≥ 0.72, @loomup/client ≥ 0.1.0, @loomup/react ≥ 0.1.0, @react-native-async-storage/async-storage ≥ 1.
Build from the monorepo:
cd packages/client && npm install && npm run build
cd ../react && npm install && npm run build
cd ../react-native && npm install && npm test
Base URL (local dev)
| Environment | Typical url |
|---|---|
| Android emulator | http://10.0.2.2:3000 |
| iOS simulator | http://127.0.0.1:3000 |
| Physical device | http://<your-lan-ip>:3000 |
Production should use HTTPS. Cleartext HTTP may be blocked (Android network security config / iOS ATS) — enable only for local development.
Quick start
import AsyncStorage from "@react-native-async-storage/async-storage";
import {
createNativeClient,
LoomupNativeProvider,
useAuth,
useLiveQuery,
useMutation,
} from "@loomup/react-native";
import { View, Text, Button, FlatList } from "react-native";
// Keep the client stable (module scope).
const client = createNativeClient({ url: "http://10.0.2.2:3000" });
export function App() {
return (
<LoomupNativeProvider client={client} asyncStorage={AsyncStorage}>
<Todos />
</LoomupNativeProvider>
);
}
function Todos() {
const { user, loading: authLoading, signIn, signOut } = useAuth();
const { data, loading, ready } = useLiveQuery("todos", {
sort: "-id",
limit: 50,
strategy: "merge",
});
const { mutate: addTodo } = useMutation((c, title: string) =>
c.from("todos").insert({ title, completed: 0 }),
);
if (authLoading) return <Text>Loading…</Text>;
if (!user) {
return (
<Button
title="Sign in"
onPress={() =>
signIn({ email: "a@b.com", password: "secret12" })
}
/>
);
}
return (
<View>
<Text>{user.email}</Text>
<Button title="Sign out" onPress={() => signOut()} />
<Button title="Add" onPress={() => addTodo("Ship RN SDK")} />
{loading && <Text>Loading todos…</Text>}
<FlatList
data={data ?? []}
keyExtractor={(row) => String(row.id)}
renderItem={({ item }) => <Text>{String(item.title)}</Text>}
/>
{ready && <Text>Live</Text>}
</View>
);
}
Session persistence defaults to on (persist.enabled defaults to true) and stores access + refresh tokens in AsyncStorage under keys loomup:access / loomup:refresh.
API
createNativeClient(options)
Same as createClient from @loomup/client. React Native provides global fetch and WebSocket. Pass WebSocketImpl only if you need an override.
LoomupNativeProvider
| Prop | Description |
|---|---|
client | Existing LoomupClient (preferred). |
options | CreateClientOptions when client is omitted. |
asyncStorage | AsyncStorage module (required when persist is enabled and persist.storage is not set). |
persist.enabled | Default true (unlike web React SDK). |
persist.storageKey | Key prefix (default loomup). |
persist.storage | Custom TokenStorage (skips AsyncStorage). |
Hooks
All hooks from @loomup/react are re-exported: useAuth, useSelect, useRow, useMutation, useSubscribe, useLiveQuery, useLoomup.
Push tokens
Use the underlying client after OS permission + token acquisition (Expo Notifications / FCM). Loomup does not embed those libraries.
import { useLoomup } from "@loomup/react-native";
const client = useLoomup();
await client.push.registerDevice({
token: expoPushToken,
provider: "expo",
platform: "ios",
});
See Push notifications.
asyncStorageAdapter(storage)
Maps AsyncStorage (or any { getItem, setItem, removeItem } store) to TokenStorage for use with LoomupProvider / persist.storage.
SecureStore (optional)
AsyncStorage is not encrypted. For stronger token storage, wrap Expo SecureStore (or similar) as TokenStorage:
import * as SecureStore from "expo-secure-store";
import type { TokenStorage } from "@loomup/react-native";
const secureStorage: TokenStorage = {
getItem: (key) => SecureStore.getItemAsync(key),
setItem: (key, value) => SecureStore.setItemAsync(key, value),
removeItem: (key) => SecureStore.deleteItemAsync(key),
};
// <LoomupNativeProvider
// client={client}
// persist={{ enabled: true, storage: secureStorage }}
// />
Realtime & backgrounding
The TypeScript client reconnects with exponential backoff after unexpected WebSocket closes and re-sends subscriptions, then RESYNC catch-up. When the app is backgrounded, the OS may close the socket; on return to foreground the client reconnects automatically if subscriptions are still active. You do not need a separate AppState helper for v1.
Types
Same as the React SDK — generate table maps with:
loomup gen typescript --config loomup.toml --output ./loomup-types.ts
import { createNativeClient } from "@loomup/react-native";
import type { TableMap, TableInsertMap, TableUpdateMap } from "./loomup-types";
const client = createNativeClient<TableMap, TableInsertMap, TableUpdateMap>({
url: "https://api.example.com",
});
Publish workflow (maintainers)
- Publish or link
@loomup/clientand@loomup/reactfirst (react needs theTokenStorageAPI). - Bump
packages/react-native/package.jsonversion. npm test/npm run buildinpackages/react-native.npm publish --access public.
Offline SQLite cache
sqliteSyncStorage(await SQLite.openDatabaseAsync("loomup-local.db"))
adapts Expo SQLite to the canonical SyncStore. See Offline sync v1.
Non-goals (v1)
- Bundling a specific SQLite native module (the adapter accepts a structural async database).
- Native modules (JSI / TurboModules).
- Default SecureStore (use custom
TokenStorage). - Full Expo example app in the monorepo (docs-only; optional follow-up).