Amba

React SDK

React hooks for amba — useAmba, useUser, useCollection, useFlag, useEntitlement, useVariant, useTrackOnMount. Companion to @layers/amba-web.

@layers/amba-react wraps @layers/amba-web in reactive hooks that re-render when SDK state changes. Same imperative surface is still available through useAmba() for one-off calls — the hooks just add the missing reactivity layer.

Works with React 18 and React 19 (server components are not supported — every hook is a client hook).

1. Install

pnpm add @layers/amba-react @layers/amba-web react

react is a peer dependency; the package will use whichever React version your app has installed. @layers/amba-web is the underlying SDK — the hooks are a thin reactive layer over it.

2. Configure before render

Configure the underlying @layers/amba-web SDK before rendering <AmbaProvider>. The provider does not configure the SDK itself — it just plugs the React tree into the SDK's state-change channel.

// src/main.tsx (Vite)
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Amba } from '@layers/amba-web';
import { AmbaProvider } from '@layers/amba-react';
import App from './App';
 
await Amba.configure({
  projectId: import.meta.env.VITE_AMBA_PROJECT_ID!,
  clientKey: import.meta.env.VITE_AMBA_CLIENT_KEY!,
});
 
ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <AmbaProvider>
      <App />
    </AmbaProvider>
  </React.StrictMode>,
);

For Next.js, do the configure call inside a client-side bootstrap component that mounts at the root and wraps children in <AmbaProvider>. Use NEXT_PUBLIC_AMBA_PROJECT_ID and NEXT_PUBLIC_AMBA_CLIENT_KEY in your .env.local.

3. First hook: useUser

import { useUser } from '@layers/amba-react';
 
function Header() {
  const { user, isAuthenticated, loading } = useUser();
 
  if (loading) return <Skeleton />;
  if (!isAuthenticated) return <button onClick={signIn}>Sign in</button>;
  return <span>{user?.display_name ?? user?.email}</span>;
}
 
async function signIn() {
  // Re-uses the underlying @layers/amba-web SDK; the hook re-renders
  // automatically once the session is established.
  const { Amba } = await import('@layers/amba-web');
  await Amba.auth.signInAnonymously();
}

4. First collection: useCollection

import { useCollection } from '@layers/amba-react';
 
type Todo = { id: string; title: string; done: boolean };
 
function TodoList() {
  const {
    data: todos,
    loading,
    error,
    refetch,
  } = useCollection<Todo>('todos', {
    order: [{ column: 'created_at', direction: 'desc' }],
    limit: 50,
  });
 
  if (loading) return <Spinner />;
  if (error) return <p>Failed to load todos: {error.message}</p>;
 
  return (
    <>
      <button onClick={refetch}>Refresh</button>
      <ul>
        {todos?.map((t) => (
          <li key={t.id}>{t.title}</li>
        ))}
      </ul>
    </>
  );
}

useCollection re-fetches on mount, on options change, and whenever the SDK auth state changes. response.next_cursor is exposed on the returned response for cursor pagination.

5. Flags and entitlements

import { useFlag, useVariant, useEntitlement } from '@layers/amba-react';
 
function NewOnboarding() {
  const flag = useFlag('new_onboarding'); // FlagAssignment | null
  const variant = useVariant('signup_copy'); // 'control' | 'short' | 'long' | null
  const isPro = useEntitlement('pro');
 
  // `useFlag(name)` returns the full assignment record (or null while loading
  // / when the flag is not assigned to this user). Read `.enabled` for the
  // boolean gate.
  if (!flag?.enabled) return <ClassicOnboarding />;
  return <NewFlow copy={variant ?? 'control'} pro={isPro} />;
}

useEntitlement defaults to false while loading — safe for paywall gating (no flash-of-pro-content).

6. Track on mount

For page-view tracking:

import { useTrackOnMount } from '@layers/amba-react';
 
export default function HomePage() {
  useTrackOnMount('page_viewed', { path: '/home' });
  return <main>...</main>;
}

The track call is fire-and-forget; failures are swallowed so a flaky network doesn't crash render.

All hooks at a glance

@layers/amba-react exports the hooks below. A resource hook returns { data, loading, error, refetch } plus the helpers listed.

Every hook calls the matching @layers/amba-web method, so the SDK reference for each namespace applies; the Notes column lists the hooks whose arguments differ from the SDK call. useDeepLinks().create posts to a client route the API does not have, so it fails; create deep links with the admin API or amba_tracked_links_create.

Core

HookReturnsNotes
useAmba()Raw Amba SDK handleRe-renders on auth state change.
useAuth(){ user, isAuthenticated, isLoading, error, signInAnonymously, signInWithEmail, signUpWithEmail, signInWithApple, signInWithGoogle, requestEmailOtp, verifyEmailOtp, requestSmsOtp, verifySmsOtp, requestMagicLink, verifyMagicLink, linkAccount, signOut, refresh }Single entry point for the auth surface.
useUser(){ user, isAuthenticated, loading, error, update }Fetches auth.me() when authenticated.
useEvents(){ track }Stable callback that fires a tracked event.
useCollection(name, options){ data, loading, error, refetch, response, insert, update, delete }Re-fetches on options JSON change + auth change.
useFlag(name)FlagAssignment | nullRead .enabled for the boolean gate. null while loading or when unassigned.
useFlags(){ data, loading, error, refetch }Bulk flag fetch; useFlag reads from it.
useVariant(name)string | nullMulti-variant flag assignment.
useEntitlement(name)booleanDefaults to false while loading.
useEntitlements(){ data, loading, error, refetch }Full list of entitlement rows.
useConfig(){ data, loading, error, refetch }data is the remote-config bundle ({ version, values }).
usePush(){ tokens, loading, error, refetch, register, unregister, subscribe, unsubscribe }Push-token + topic-subscription helpers.
useMedia(){ presign, upload, list, delete, download }presign / upload take the storage params object; delete / download take an asset id.
useTrackOnMount(event, props)side-effect — no returnFires once on mount; never throws.

Social

HookReturnsNotes
useFriends(){ data, loading, error, refetch, sendRequest, acceptRequest, declineRequest, removeFriend, blockUser, unblockUser }
useGroups(){ data, loading, error, refetch, create, get, update, delete, members, join, leave, invite }No list fetch; data stays null. create takes a GroupCreate.
useMessaging(){ data, loading, error, refetch, createConversation, listMessages, sendMessage, markRead }data holds messaging.conversations(). listMessages(id, { limit, offset }).
useFeeds(feed?){ data, loading, error, refetch }Activity feed; feed defaults to "default".
useReviews(targetType?, targetId?){ data, loading, error, refetch, create, update, delete }data is reviews.list(targetType, targetId); no fetch until both are set.

Lifecycle

HookReturnsNotes
useContent(libraryKey?){ data, loading, error, refetch, item }Without a key, data is content.today().
useOnboarding(){ status, loading, error, refetch, nextStep, skipStep, complete }nextStep(payload?) sends payload (default {}).
useDeepLinks(){ get, create }

Gamification and economy

HookReturnsNotes
useAchievements(){ data, loading, error, refetch, progress }data is achievements.getAll(); progress() is getProgress().
useXp(){ data, loading, error, refetch, history, claim }data is xp.getBalance(); history(limit?).
useStreaks(){ data, loading, error, refetch, qualify }data is streaks.getAll().
useLeaderboards(key?){ data, loading, error, refetch, entries, myRank }Fetches leaderboards.get(key) with a key.
useLeagues(){ data, loading, error, refetch, cohort }data is leagues.me().
useChallenges(){ data, loading, error, refetch, get, progress, claim }
useCurrencies(currencyCode?){ data, loading, error, refetch, transactions }data is currencies.getBalance(), narrowed to one currency when a code is given.
useInventory(){ data, loading, error, refetch, item, purchase, consume }consume(itemId, quantity = 1); purchase resolves with the receipt.
useStore(key?){ data, loading, error, refetch, purchase }Without a key, data lists the stores.

Escape hatch: raw SDK

When you need something the hooks don't cover yet (e.g. storage.upload, ai.anthropic.messages.create, push.register), use useAmba():

import { useAmba } from '@layers/amba-react';
 
function UploadAvatar() {
  const Amba = useAmba();
  async function onFileChange(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0];
    if (!file) return;
    const asset = await Amba.storage.upload({ bucket: 'avatars', file });
    console.log(asset.url);
  }
  return <input type="file" onChange={onFileChange} />;
}

useAmba() is just Amba from @layers/amba-web plus subscription to the version counter — so the component re-renders when the SDK's auth state changes.

Server components

Server components run before any client SDK is configured. Don't import @layers/amba-react (or @layers/amba-web) from server-only modules.

Common pitfalls

  • Hook called outside <AmbaProvider> throws "hooks must be used inside <AmbaProvider>". Wrap the root.
  • Amba.configure() not awaited before render causes hooks to throw "amba SDK not configured". Either await it before the first render, or render a <SDKLoading /> boundary that flips when Amba.appUserId is no longer undefined.
  • Two copies of @layers/amba-web — useUser, useFlag, etc. all import the same singleton from @layers/amba-web. If pnpm why @layers/amba-web shows two copies, the hooks subscribe to one and your Amba.configure() call configures the other. Hoist with pnpm dedupe.

See also

On this page