React SDK
@vaifstudio/react puts @vaifstudio/client behind a provider and hooks. The provider keeps the session in the browser, refreshes it before it expires, and makes every request run as the signed-in user.
Install
Section titled “Install”npm install @vaifstudio/react @vaifstudio/clientIt needs React 18 or later.
Set up the provider
Section titled “Set up the provider”Create the client once and pass it to VaifProvider:
import type { ReactNode } from "react";import { createVaifClient } from "@vaifstudio/client";import { VaifProvider } from "@vaifstudio/react";
const vaif = createVaifClient({ projectId: "your-project-id", apiKey: "vk_pub_xxx",});
export function Providers({ children }: { children: ReactNode }) { return <VaifProvider client={vaif}>{children}</VaifProvider>;}<VaifProvider config={{ projectId, apiKey }}> creates the client for you instead. Inside the tree, useVaifClient() returns it.
Sign users in
Section titled “Sign users in”import { useState } from "react";import { isVaifError } from "@vaifstudio/client";import { useAuth, useMFA } from "@vaifstudio/react";
export function SignIn() { const { user, isLoading, signIn, signOut } = useAuth(); const { verify } = useMFA(); const [challenge, setChallenge] = useState<string | null>(null);
if (isLoading) return <p>Loading…</p>; if (user) return <button onClick={() => signOut()}>Sign out {user.email}</button>;
const onSubmit = async (email: string, password: string) => { try { await signIn(email, password); } catch (e) { if (isVaifError(e) && e.code === "MFA_REQUIRED") { setChallenge((e.details as { challengeToken: string }).challengeToken); } else { throw e; } } };
if (challenge) return <CodeForm onSubmit={(code) => verify(challenge, code)} />; return <CredentialsForm onSubmit={onSubmit} />;}
declare function CodeForm(props: { onSubmit: (code: string) => void }): JSX.Element;declare function CredentialsForm(props: { onSubmit: (email: string, password: string) => void }): JSX.Element;What the provider does for you:
- Every request runs as the signed-in user. Data, storage, functions and the realtime connection all carry the user’s token. Signing out clears it.
- The session persists in
localStorage(keyvaif-auth, whichstorageKeychanges) and is checked with the API on the next load. - Refresh is automatic and cannot loop. A minute before the access token expires, the provider refreshes it once. A failed refresh signs the user out instead of retrying.
autoRefreshSession={false}turns refresh off. - Tabs stay in step. Signing in, refreshing or signing out in one tab updates the other tabs of the same app.
- Signing out ends every session the user has, on every device.
| Hook | Returns |
|---|---|
useUser(), useToken() |
The current user or access token, or null |
usePasswordReset() |
requestReset(email) and confirmReset(token, newPassword) |
useEmailVerification() |
requestVerification(email) and confirmVerification(token) |
useMagicLink() |
sendMagicLink(email, redirectUrl?), and verifyMagicLink() on the page the link opens |
useMFA() |
enrollStart(), enrollConfirm(enrollToken, code), verify(challengeToken, code), disable(code) and regenerateCodes(code) |
useOAuth() |
signInWithOAuth("google" | "github", redirectUrl?) |
Google and GitHub sign-in need a paid plan, the provider set up in the console under Authentication > OAuth Apps, and the return page’s origin on the project’s redirect URLs. On the return page, call client.auth.handleOAuthCallback() and pass the result to setSession() from useVaif().
import { useQuery, useCreate, useUpdate, useDelete } from "@vaifstudio/react";
type Post = { id: string; title: string; published: boolean; created_at: string };
export function Posts() { const { data: posts, isLoading, error, refetch } = useQuery<Post>("posts", { where: { field: "published", operator: "eq", value: true }, orderBy: { field: "created_at", direction: "desc" }, limit: 20, }); const create = useCreate<Post>("posts", { onSuccess: () => refetch() }); const update = useUpdate<Post>("posts"); const remove = useDelete("posts");
if (isLoading) return <p>Loading…</p>; if (error) return <p>{error.message}</p>; return ( <> <button disabled={create.isLoading} onClick={() => create.mutate({ title: "New", published: true })}> New post </button> {posts.map((p) => ( <div key={p.id}> {p.title} <button onClick={() => update.mutate({ id: p.id, data: { published: false } })}>Unpublish</button> <button onClick={() => remove.mutate(p.id)}>Delete</button> </div> ))} </> );}A query runs again only when its options change, when enabled turns on, or when the signed-in user changes, so options can be written inline. The data hooks wait for the provider to restore a stored session before their first request.
Also available: useQueryById, useQueryFirst, usePaginatedQuery, useInfiniteQuery, useCount, useUpsert, useBatchCreate, useBatchUpdate, useBatchDelete, useOptimisticMutation and useMutation(fn). Mutations are not retried unless you pass retry.
Realtime
Section titled “Realtime”All realtime hooks share the client’s one connection. See Realtime in React for useRealtime, useSubscription, useChannel, useBroadcast, usePresence and useRealtimeConnection.
Storage and functions
Section titled “Storage and functions”import { useUpload, useFunction } from "@vaifstudio/react";
export function Avatar() { const { upload, isUploading, error } = useUpload({ bucket: "avatars" }); const { invoke, isLoading } = useFunction<{ to: string }, { sent: boolean }>("send-email");
return ( <div> <input type="file" onChange={(e) => e.target.files?.[0] && upload(`me/${e.target.files[0].name}`, e.target.files[0])} /> {isUploading && <p>Uploading…</p>} {error && <p>{error.message}</p>} <button disabled={isLoading} onClick={() => invoke({ to: "sam@example.com" })}> Send </button> </div> );}Also available: useDownload, useFile, useFiles, useDropzone, usePublicUrl, useFunctionList and useBatchInvoke. Listing files and buckets needs a project developer’s console token.