Frontend SDK: React Hooks
The Optare React SDK provides hooks and utilities for managing authentication state in your frontend application. These hooks make it easy to access user data, check authentication status, and control access to UI elements.
Installation
The React hooks are included in the Better Auth client:
pnpm add better-authConfiguration
Create your auth client in lib/auth-client.ts:
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000",
});
export const { useSession, signIn, signUp, signOut } = authClient;Core Hooks
useSession
Returns the current user's session, including authentication status, user data, organization, and role.
Signature:
function useSession(): {
data: Session | null;
isPending: boolean;
error: Error | null;
}Session Object:
interface Session {
user: {
id: string;
name: string;
email: string;
image?: string;
emailVerified: boolean;
};
organization?: {
id: string;
name: string;
slug: string;
};
role?: "owner" | "admin" | "member";
expiresAt: string;
}Example:
import { useSession } from "~/lib/auth-client";
export function UserProfile() {
const { data: session, isPending, error } = useSession();
if (isPending) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error.message}</div>;
}
if (!session) {
return <div>Not signed in</div>;
}
return (
<div>
<h2>Welcome, {session.user.name}!</h2>
<p>Email: {session.user.email}</p>
{session.organization && (
<p>Organization: {session.organization.name}</p>
)}
{session.role && <p>Role: {session.role}</p>}
</div>
);
}Authentication Functions
signIn.social
Initiates OAuth login with Optare.
Signature:
function signIn.social(options: {
provider: "optare";
callbackURL?: string;
}): Promise<void>Example:
import { signIn } from "~/lib/auth-client";
export function SignInButton() {
const handleSignIn = async () => {
await signIn.social({
provider: "optare",
callbackURL: "/dashboard",
});
};
return (
<button onClick={handleSignIn}>
Sign in with Optare
</button>
);
}signOut
Signs out the current user and clears their session.
Signature:
function signOut(options?: {
callbackURL?: string;
}): Promise<void>Example:
import { signOut } from "~/lib/auth-client";
export function SignOutButton() {
const handleSignOut = async () => {
await signOut({
callbackURL: "/",
});
};
return (
<button onClick={handleSignOut}>
Sign Out
</button>
);
}Common Patterns
Protecting Components
Only render content if the user is authenticated:
import { useSession } from "~/lib/auth-client";
export function AdminPanel() {
const { data: session } = useSession();
if (!session) {
return <div>Please sign in to access this page.</div>;
}
return (
<div>
<h1>Admin Panel</h1>
{/* Admin content */}
</div>
);
}Role-Based Rendering
Show different UI based on the user's role:
import { useSession } from "~/lib/auth-client";
export function TeamSettings() {
const { data: session } = useSession();
return (
<div>
<h1>Team Settings</h1>
{/* Everyone can see basic info */}
<div>
<p>Organization: {session?.organization?.name}</p>
</div>
{/* Only admins and owners can invite members */}
{(session?.role === "admin" || session?.role === "owner") && (
<div>
<h2>Invite Members</h2>
<InviteMemberForm />
</div>
)}
{/* Only owners can delete the organization */}
{session?.role === "owner" && (
<div>
<h2>Danger Zone</h2>
<DeleteOrganizationButton />
</div>
)}
</div>
);
}Loading States
Handle loading and error states gracefully:
import { useSession } from "~/lib/auth-client";
export function Dashboard() {
const { data: session, isPending, error } = useSession();
if (isPending) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900"></div>
</div>
);
}
if (error) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-red-600">
<h2>Error loading session</h2>
<p>{error.message}</p>
</div>
</div>
);
}
if (!session) {
return <Navigate to="/sign-in" />;
}
return (
<div>
<h1>Dashboard</h1>
{/* Dashboard content */}
</div>
);
}Protected Route Wrapper
Create a reusable component for protected routes:
import { useSession } from "~/lib/auth-client";
import { Navigate } from "@remix-run/react"; // or next/navigation
import { ReactNode } from "react";
interface ProtectedRouteProps {
children: ReactNode;
requiredRole?: "owner" | "admin" | "member";
}
export function ProtectedRoute({ children, requiredRole }: ProtectedRouteProps) {
const { data: session, isPending } = useSession();
if (isPending) {
return <div>Loading...</div>;
}
if (!session) {
return <Navigate to="/sign-in" />;
}
if (requiredRole) {
const roleHierarchy = { owner: 3, admin: 2, member: 1 };
const userRoleLevel = roleHierarchy[session.role || "member"];
const requiredRoleLevel = roleHierarchy[requiredRole];
if (userRoleLevel < requiredRoleLevel) {
return <Navigate to="/dashboard" />;
}
}
return <>{children}</>;
}Usage:
// In Next.js App Router
export default function AdminPage() {
return (
<ProtectedRoute requiredRole="admin">
<div>
<h1>Admin Dashboard</h1>
{/* Admin content */}
</div>
</ProtectedRoute>
);
}User Avatar Component
Build a user menu with session data:
import { useSession, signOut } from "~/lib/auth-client";
import { useState } from "react";
export function UserMenu() {
const { data: session } = useSession();
const [isOpen, setIsOpen] = useState(false);
if (!session) {
return null;
}
return (
<div className="relative">
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center space-x-2"
>
{session.user.image ? (
<img
src={session.user.image}
alt={session.user.name}
className="w-8 h-8 rounded-full"
/>
) : (
<div className="w-8 h-8 rounded-full bg-gray-300 flex items-center justify-center">
{session.user.name[0].toUpperCase()}
</div>
)}
<span>{session.user.name}</span>
</button>
{isOpen && (
<div className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1">
<div className="px-4 py-2 text-sm text-gray-700 border-b">
<p className="font-medium">{session.user.name}</p>
<p className="text-gray-500">{session.user.email}</p>
</div>
{session.organization && (
<div className="px-4 py-2 text-sm text-gray-700 border-b">
<p className="text-gray-500">Organization</p>
<p className="font-medium">{session.organization.name}</p>
<p className="text-xs text-gray-400 capitalize">{session.role}</p>
</div>
)}
<button
onClick={() => signOut()}
className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
Sign out
</button>
</div>
)}
</div>
);
}Best Practices
1. Always Check Loading State
Before rendering based on session data, check isPending:
const { data: session, isPending } = useSession();
if (isPending) {
return <LoadingSpinner />;
}
// Now safe to check session
if (!session) {
return <Navigate to="/sign-in" />;
}2. Handle Null Sessions
The session can be null even after loading completes:
const { data: session } = useSession();
// ✅ Good - safely access nested properties
const orgName = session?.organization?.name || "No organization";
// ❌ Bad - will crash if session is null
const orgName = session.organization.name;3. Cache Session Data
The useSession hook automatically caches session data. Don't create multiple instances of the auth client.
// ✅ Good - single auth client exported
// lib/auth-client.ts
export const authClient = createAuthClient({ ... });
export const { useSession } = authClient;
// ❌ Bad - creates new client instance
function MyComponent() {
const client = createAuthClient({ ... });
const { data } = client.useSession();
}4. Use TypeScript
The SDK is fully typed. Use TypeScript to catch errors at compile time:
import { useSession } from "~/lib/auth-client";
export function Example() {
const { data: session } = useSession();
// TypeScript knows session.role can only be specific values
if (session?.role === "owner") {
// Owner-only logic
}
}Next Steps
- Organizations - Learn about multi-tenancy
UI Components
The @optare/optareid-react package includes pre-built UI components for common authentication flows.
<SignIn />
A complete sign-in form that handles email/password and social login.
import { SignIn } from '@optare/optareid-react';
export function LoginPage() {
return (
<div className="login-container">
<SignIn
onSuccess={(token) => console.log('Logged in!', token)}
onError={(err) => console.error('Login failed', err)}
/>
</div>
);
}<UserProfile />
Displays the current user's profile and a sign-out button.
import { UserProfile, useOptare } from '@optare/optareid-react';
export function Header() {
const { auth } = useOptare();
const user = { name: 'Alice', email: 'alice@example.com' }; // Get from auth.getMe()
return (
<header>
<UserProfile
user={user}
onSignOut={() => auth.signOut()}
/>
</header>
);
}