---
title: "React integration"
description: "An auth context, a useAuth hook, and route guarding for React apps built on the Rayfin auth client with Fabric SSO."
url: https://rayfin.ai/docs/auth/react
markdown_url: https://rayfin.ai/docs/auth/react.md
section: auth
product: Rayfin
sdk_version: 1.34.0
cli_version: 1.33.2
last_updated: 2026-08-23T15:47:25-07:00
source: auth/react.mdx
---

# React integration

> An auth context, a useAuth hook, and route guarding for React apps built on the Rayfin auth client with Fabric SSO.

Rayfin's auth client is framework-agnostic — `getSession` and `onSessionChange` work the
same everywhere, and Fabric SSO's `ensureSignedInWithFabric` resolves to the same opaque
session shape. This page wires them into React with a context, a hook, and route guarding.

## A minimal `useAuth` hook [#a-minimal-useauth-hook]

The smallest useful integration reads the current session once on mount, subscribes to
changes, and exposes a `signIn` that wraps `ensureSignedInWithFabric`:

```typescript title="src/hooks/useAuth.ts"
import { useState, useEffect, useCallback } from 'react';
import { ensureSignedInWithFabric } from '@microsoft/rayfin-auth-provider-fabric';
import { auth } from '../services/rayfinClient';
import type { OpaqueSession } from '@microsoft/rayfin-auth';

const fabricOptions = {
  workspaceId: import.meta.env.VITE_FABRIC_WORKSPACE_ID,
  projectId: import.meta.env.VITE_FABRIC_ITEM_ID,
  fabricPortalUrl: import.meta.env.VITE_FABRIC_PORTAL_URL,
  returnOrigin: window.location.origin,
};

export function useAuth() {
  const [session, setSession] = useState<OpaqueSession | null>(null);

  useEffect(() => {
    setSession(auth.getSession());
    return auth.onSessionChange(setSession);
  }, []);

  const signIn = useCallback(() => ensureSignedInWithFabric(auth, fabricOptions), []);

  return {
    ...session,
    isAuthenticated: session?.isAuthenticated ?? false,
    signIn,
    signOut: auth.signOut.bind(auth),
  };
}
```

This is enough for a single-page app. Most real apps need one more thing: a shared provider
so every component sees the same session without each one re-subscribing to
`onSessionChange` itself. The rest of this page builds that.

## An auth context and provider [#an-auth-context-and-provider]

`src/services/rayfinClient.ts` creates the shared `RayfinClient` and the `FabricAuthOptions`
the provider needs:

```typescript title="src/services/rayfinClient.ts"
import { RayfinClient } from '@microsoft/rayfin-client';

export const client = new RayfinClient({
  baseUrl: import.meta.env.VITE_RAYFIN_API_URL,
  publishableKey: import.meta.env.VITE_RAYFIN_PUBLISHABLE_KEY,
});

export const auth = client.auth;

export const fabricOptions = {
  workspaceId: import.meta.env.VITE_FABRIC_WORKSPACE_ID,
  projectId: import.meta.env.VITE_FABRIC_ITEM_ID,
  fabricPortalUrl: import.meta.env.VITE_FABRIC_PORTAL_URL,
  returnOrigin: window.location.origin,
};
```

The provider owns the session state, exposes loading/error state during sign-in, and hands
the same `signIn` / `signOut` functions to every consumer:

```tsx title="src/hooks/AuthContext.tsx"
import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useState,
  type ReactNode,
} from 'react';

import { ensureSignedInWithFabric } from '@microsoft/rayfin-auth-provider-fabric';
import type { OpaqueSession } from '@microsoft/rayfin-auth';
import { auth, fabricOptions } from '../services/rayfinClient';

interface AuthContextValue {
  session: OpaqueSession | null;
  loading: boolean;
  error: string | null;
  signIn: () => Promise<OpaqueSession>;
  signOut: () => Promise<void>;
  isAuthenticated: boolean;
}

const AuthContext = createContext<AuthContextValue | undefined>(undefined);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [session, setSession] = useState<OpaqueSession | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    setSession(auth.getSession());
    setLoading(false);
    return auth.onSessionChange(setSession);
  }, []);

  const signIn = useCallback(async () => {
    setError(null);
    setLoading(true);
    try {
      const result = await ensureSignedInWithFabric(auth, fabricOptions);
      setSession(result);
      return result;
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Sign-in failed');
      throw err;
    } finally {
      setLoading(false);
    }
  }, []);

  const signOut = useCallback(async () => {
    await auth.signOut();
    setSession(null);
    setError(null);
  }, []);

  const value = useMemo<AuthContextValue>(
    () => ({
      session,
      loading,
      error,
      signIn,
      signOut,
      isAuthenticated: session?.isAuthenticated ?? false,
    }),
    [session, loading, error, signIn, signOut]
  );

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export function useAuth(): AuthContextValue {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
}
```

The provider talks to `client.auth` directly and owns the session state — every component
that calls `useAuth()` sees the same session without re-subscribing to `onSessionChange`
itself.

## Guarding routes [#guarding-routes]

A guard component reads `useAuth()` and redirects based on `isAuthenticated`, showing a
loading state until the first session check resolves:

```tsx title="src/App.tsx"
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';

import { AuthPage } from './components/AuthPage';
import { useAuth } from './hooks/AuthContext';
import { HomePage } from './pages/HomePage';

function AuthGuard({
  children,
  requireAuth,
}: {
  children: React.ReactNode;
  requireAuth: boolean;
}) {
  const { isAuthenticated, loading } = useAuth();

  if (loading) return <div>Loading…</div>;
  if (requireAuth && !isAuthenticated) return <Navigate to="/auth" replace />;
  if (!requireAuth && isAuthenticated) return <Navigate to="/" replace />;

  return <>{children}</>;
}

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route
          path="/auth"
          element={
            <AuthGuard requireAuth={false}>
              <AuthPage />
            </AuthGuard>
          }
        />
        <Route
          path="/"
          element={
            <AuthGuard requireAuth={true}>
              <HomePage />
            </AuthGuard>
          }
        />
        <Route path="*" element={<Navigate to="/" replace />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;
```

## Sign-in button and app entry point [#sign-in-button-and-app-entry-point]

`ensureSignedInWithFabric`'s popup fallback calls `window.open()`, so `signIn` has to run
from a synchronous user-gesture handler — wire it to a button, not an effect:

```tsx title="src/components/AuthPage.tsx"
import { useAuth } from '../hooks/AuthContext';

export function AuthPage() {
  const { signIn, loading, error } = useAuth();

  return (
    <div>
      <button onClick={() => signIn()} disabled={loading}>
        Sign in with Microsoft Fabric
      </button>
      {error && <p role="alert">{error}</p>}
    </div>
  );
}
```

Wrap the app in `AuthProvider` at the entry point:

```tsx title="src/main.tsx"
import { createRoot } from 'react-dom/client';
import App from './App';
import { AuthProvider } from './hooks/AuthContext';

createRoot(document.getElementById('root')!).render(
  <AuthProvider>
    <App />
  </AuthProvider>
);
```

> [!NOTE]
> Sign-in completes from a local Vite dev server as long as your origin is in
> `allowedRedirectUris` and the backend is deployed — see
> [Local frontend development](/docs/auth/fabric-sso#local-frontend-development). Everything
> else in this pattern — the provider, the guard, `isAuthenticated` — behaves the same
> regardless of which flow produced the session.

```prompt title="Add an AuthProvider, useAuth hook, and route guarding"
In my Rayfin React app (Vite + react-router-dom), add authentication wiring:

- An AuthProvider (React context) that wraps the app, calls client.auth.getSession() once
  on mount, subscribes with client.auth.onSessionChange(...), and exposes { session,
  loading, error, signIn, signOut, isAuthenticated } through a useAuth() hook.
- signIn() should call ensureSignedInWithFabric() from
  @microsoft/rayfin-auth-provider-fabric, built from VITE_FABRIC_WORKSPACE_ID,
  VITE_FABRIC_ITEM_ID, and VITE_FABRIC_PORTAL_URL, and must be wired to a button's onClick
  so the popup isn't blocked.
- An AuthGuard component used in the router that redirects unauthenticated users to /auth
  and authenticated users away from /auth, showing a loading state in between.

Use onSessionChange, not onAuthStateChange — the latter does not exist on the Rayfin auth
client. Do not introspect session fields beyond isAuthenticated, isAnonymous, user, role,
and expiresAt. Show me the full set of files.
```
