Expo and React Native SDK
@vaifstudio/expo is the React SDK’s provider and hooks for React Native and Expo. It keeps the session in the storage you give it, refreshes it automatically, and closes the realtime connection while the app is in the background.
Install
Section titled “Install”npx expo install @vaifstudio/expo @vaifstudio/client @react-native-async-storage/async-storageIt needs React Native 0.70 or later (Expo SDK 47 or later). @react-native-async-storage/async-storage is optional: without it, the session lasts only while the app runs.
Set up the provider
Section titled “Set up the provider”import type { ReactNode } from "react";import AsyncStorage from "@react-native-async-storage/async-storage";import { VaifProvider } from "@vaifstudio/expo";
export function Providers({ children }: { children: ReactNode }) { return ( <VaifProvider config={{ projectId: process.env.EXPO_PUBLIC_VAIF_PROJECT_ID!, apiKey: process.env.EXPO_PUBLIC_VAIF_PUBLISHABLE_KEY!, // the publishable vk_pub_ key }} storage={AsyncStorage} > {children} </VaifProvider> );}Pass client={createVaifClient(...)} instead of config to share a client you created. Inside the tree, useVaifClient() returns it.
Sign users in
Section titled “Sign users in”import { useState } from "react";import { Button, Text, TextInput, View } from "react-native";import { isVaifError } from "@vaifstudio/client";import { useAuth, useMFA } from "@vaifstudio/expo";
export function SignInScreen() { const { user, isLoading, signIn, signOut } = useAuth(); const { verify } = useMFA(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [code, setCode] = useState(""); const [challenge, setChallenge] = useState<string | null>(null);
if (isLoading) return <Text>Loading…</Text>; if (user) return <Button title={`Sign out ${user.email}`} onPress={() => signOut()} />;
if (challenge) { return ( <View> <TextInput value={code} onChangeText={setCode} keyboardType="number-pad" /> <Button title="Verify" onPress={() => verify(challenge, code)} /> </View> ); }
const submit = async () => { try { await signIn(email, password); } catch (e) { if (isVaifError(e) && e.code === "MFA_REQUIRED") { setChallenge((e.details as { challengeToken: string }).challengeToken); } } };
return ( <View> <TextInput value={email} onChangeText={setEmail} autoCapitalize="none" /> <TextInput value={password} onChangeText={setPassword} secureTextEntry /> <Button title="Sign in" onPress={submit} /> </View> );}The provider keeps the session, including the refresh token, in the storage you pass (key vaif-auth) and checks it with the API on the next launch. It refreshes the access token once, a minute before it expires, and signs the user out if the refresh fails.
Other auth hooks: useUser, useToken, usePasswordReset, useEmailVerification, useMagicLink (send a link that opens your app, then verifyMagicLink(accessToken) with the token from the link) and useMFA. Google and GitHub sign-in for your app’s users is not available in the Expo SDK yet.
import { FlatList, Text } from "react-native";import { useQuery, useCreate } from "@vaifstudio/expo";
type Note = { id: string; title: string; done: boolean };
export function Notes() { const { data, isLoading, refetch } = useQuery<Note>("notes", { where: { field: "done", operator: "eq", value: false }, limit: 50, }); const { create } = useCreate<Note>("notes");
const duplicate = async (note: Note) => { await create({ title: "Copy of " + note.title, done: false }); await refetch(); };
if (isLoading) return <Text>Loading…</Text>; return ( <FlatList data={data} keyExtractor={(n) => n.id} renderItem={({ item }) => <Text onPress={() => duplicate(item)}>{item.title}</Text>} /> );}Realtime
Section titled “Realtime”The hooks share the client’s one connection and close it while the app is in the background. Subscriptions are kept and resume when the app returns. Pass pauseInBackground: false to keep a hook’s connection open.
import { useState } from "react";import { Text } from "react-native";import { useRealtime } from "@vaifstudio/expo";
type Todo = { id: string; list_id: string; title: string };
export function LiveCount({ listId }: { listId: string }) { const [count, setCount] = useState(0); const { status } = useRealtime<Todo>("todos", { filter: { field: "list_id", value: listId }, onInsert: () => setCount((c) => c + 1), }); return <Text>{count} new {status === "SUBSCRIBED" ? "(live)" : ""}</Text>;}Also available: useSubscription, useChannel, useBroadcast, usePresence, useRealtimeConnection and useAppState.
Storage and functions
Section titled “Storage and functions”import { Button } from "react-native";import { useUpload, useFunction } from "@vaifstudio/expo";
export function UploadPhoto({ uri }: { uri: string }) { const { uploadUri, isUploading } = useUpload("photos"); const { invoke } = useFunction<{ ok: boolean }, { key: string }>("make-thumbnail");
const go = async () => { const result = await uploadUri(uri, `me/${Date.now()}.jpg`, { contentType: "image/jpeg" }); if (result) await invoke({ key: result.key }); };
return <Button title={isUploading ? "Uploading…" : "Upload"} onPress={go} disabled={isUploading} />;}uploadUri reads a local file URI, such as a photo from the camera roll, and uploads it.