React Hooks

Using useAuth, AuthProvider, and getServerSession

React Hooks

The SDK provides React hooks for seamless client-side authentication in React and Next.js applications. These hooks handle session management, loading states, and authentication actions automatically.

Overview

The SDK includes three main components for React integration:

  • AuthProvider - Context provider for managing session state
  • useAuth() - Hook for accessing session, loading state, and auth methods
  • getServerSession() - Server-side session helper for Server Components

Installation

npm install @warpy-auth-sdk/core
# The hooks are included in the core package

AuthProvider

The AuthProvider must wrap your application to provide authentication context to all child components.

Basic Setup

app/layout.tsx

Root layout with AuthProvider

import { AuthProvider } from "@warpy-auth-sdk/core/hooks";
import "./globals.css";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <AuthProvider>
          {children}
        </AuthProvider>
      </body>
    </html>
  );
}

How It Works

The AuthProvider:

  • Fetches the current session from GET /api/auth/session
  • Stores session state in React Context
  • Provides signOut and refetch methods
  • Handles loading states automatically
  • Re-fetches session on mount and window focus

useAuth Hook

The useAuth hook provides access to the current session, loading state, and authentication methods.

Return Values

interface UseAuthReturn {
  // Current session (null if not authenticated)
  session: Session | null;

  // Loading state (true during initial fetch)
  loading: boolean;

  // Sign out the current user
  signOut: () => Promise<void>;

  // Manually refetch the session
  refetch: () => Promise<void>;
}

Basic Usage

Profile Component

Display user information

'use client';

import { useAuth } from "@warpy-auth-sdk/core/hooks";

export default function Profile() {
  const { session, loading } = useAuth();

  if (loading) {
    return <div>Loading...</div>;
  }

  if (!session) {
    return <div>Not authenticated</div>;
  }

  return (
    <div>
      <h1>Welcome, {session.user.name}!</h1>
      <p>Email: {session.user.email}</p>
      {session.user.picture && (
        <img src={session.user.picture} alt="Profile" />
      )}
    </div>
  );
}

Protected Component

Protected Dashboard

Redirect unauthenticated users

'use client';

import { useAuth } from "@warpy-auth-sdk/core/hooks";
import { useRouter } from "next/navigation";
import { useEffect } from "react";

export default function Dashboard() {
  const { session, loading } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (!loading && !session) {
      router.push("/login");
    }
  }, [session, loading, router]);

  if (loading) {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <div className="animate-spin rounded-full h-32 w-32 border-b-2 border-gray-900" />
      </div>
    );
  }

  if (!session) {
    return null; // Will redirect
  }

  return (
    <div>
      <h1>Dashboard</h1>
      <p>Welcome, {session.user.name}</p>
    </div>
  );
}

Sign Out Button

Sign Out Component

Sign out the current user

'use client';

import { useAuth } from "@warpy-auth-sdk/core/hooks";
import { useRouter } from "next/navigation";

export default function SignOutButton() {
  const { signOut } = useAuth();
  const router = useRouter();

  const handleSignOut = async () => {
    await signOut();
    router.push("/login");
  };

  return (
    <button
      onClick={handleSignOut}
      className="bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700"
    >
      Sign Out
    </button>
  );
}

Conditional Rendering

Conditional Navigation

Show different UI based on auth state

'use client';

import { useAuth } from "@warpy-auth-sdk/core/hooks";
import Link from "next/link";

export default function Navigation() {
  const { session, loading, signOut } = useAuth();

  if (loading) {
    return (
      <nav>
        <div className="animate-pulse bg-gray-200 h-10 w-20 rounded" />
      </nav>
    );
  }

  return (
    <nav className="flex items-center justify-between p-4 bg-white shadow">
      <Link href="/" className="text-xl font-bold">
        My App
      </Link>

      <div className="space-x-4">
        {session ? (
          <>
            <span>Hello, {session.user.name}</span>
            <Link
              href="/dashboard"
              className="text-blue-600 hover:text-blue-800"
            >
              Dashboard
            </Link>
            <button
              onClick={signOut}
              className="text-red-600 hover:text-red-800"
            >
              Sign Out
            </button>
          </>
        ) : (
          <>
            <Link
              href="/login"
              className="text-blue-600 hover:text-blue-800"
            >
              Sign In
            </Link>
            <Link
              href="/signup"
              className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700"
            >
              Sign Up
            </Link>
          </>
        )}
      </div>
    </nav>
  );
}

getServerSession

The getServerSession helper retrieves the session in Server Components, Server Actions, and Route Handlers.

Server Component

app/profile/page.tsx

Server component with session access

import { getServerSession } from "@warpy-auth-sdk/core/hooks/server";
import { redirect } from "next/navigation";

export default async function ProfilePage() {
  const session = await getServerSession();

  if (!session) {
    redirect("/login");
  }

  return (
    <div>
      <h1>Profile</h1>
      <p>Name: {session.user.name}</p>
      <p>Email: {session.user.email}</p>
      <p>User ID: {session.user.id}</p>
    </div>
  );
}

Server Action

app/actions.ts

Server action with session validation

'use server';

import { getServerSession } from "@warpy-auth-sdk/core/hooks/server";
import { revalidatePath } from "next/cache";

export async function updateProfile(formData: FormData) {
  const session = await getServerSession();

  if (!session) {
    throw new Error("Unauthorized");
  }

  const name = formData.get("name") as string;

  // Update user in database
  await db.user.update({
    where: { id: session.user.id },
    data: { name },
  });

  revalidatePath("/profile");

  return { success: true };
}

Route Handler

app/api/user/route.ts

API route with session validation

import { getServerSession } from "@warpy-auth-sdk/core/hooks/server";
import { NextRequest } from "next/server";

export async function GET(request: NextRequest) {
  const session = await getServerSession();

  if (!session) {
    return Response.json(
      { error: "Unauthorized" },
      { status: 401 }
    );
  }

  // Fetch user data from database
  const user = await db.user.findUnique({
    where: { id: session.user.id },
    include: { posts: true },
  });

  return Response.json({ user });
}

Session Type

The session object has the following structure:

interface Session {
  // User information
  user: {
    id: string;
    email: string;
    name?: string;
    picture?: string;
  };

  // Session metadata
  expires: string; // ISO timestamp
  type?: string;   // 'oauth', 'email', 'mcp-agent', etc.

  // Custom fields (from session callback)
  [key: string]: any;
}

Advanced Patterns

Custom Loading Component

LoadingGuard Component

Reusable loading guard

'use client';

import { useAuth } from "@warpy-auth-sdk/core/hooks";
import { ReactNode } from "react";

interface LoadingGuardProps {
  children: ReactNode;
  fallback?: ReactNode;
}

export function LoadingGuard({ children, fallback }: LoadingGuardProps) {
  const { loading } = useAuth();

  if (loading) {
    return fallback || (
      <div className="flex items-center justify-center min-h-screen">
        <div className="animate-spin rounded-full h-32 w-32 border-b-2 border-gray-900" />
      </div>
    );
  }

  return <>{children}</>;
}

// Usage:
// <LoadingGuard fallback={<Spinner />}>
//   <Dashboard />
// </LoadingGuard>

Auth Guard Component

AuthGuard Component

Protect routes with a reusable guard

'use client';

import { useAuth } from "@warpy-auth-sdk/core/hooks";
import { useRouter } from "next/navigation";
import { ReactNode, useEffect } from "react";

interface AuthGuardProps {
  children: ReactNode;
  redirectTo?: string;
}

export function AuthGuard({ children, redirectTo = "/login" }: AuthGuardProps) {
  const { session, loading } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (!loading && !session) {
      router.push(redirectTo);
    }
  }, [session, loading, router, redirectTo]);

  if (loading) {
    return <div>Loading...</div>;
  }

  if (!session) {
    return null; // Will redirect
  }

  return <>{children}</>;
}

// Usage:
// <AuthGuard redirectTo="/login">
//   <ProtectedContent />
// </AuthGuard>

Role-Based Access

Role Guard Component

Protect routes by user role

'use client';

import { useAuth } from "@warpy-auth-sdk/core/hooks";
import { useRouter } from "next/navigation";
import { ReactNode, useEffect } from "react";

interface RoleGuardProps {
  children: ReactNode;
  requiredRole: string;
  fallback?: ReactNode;
}

export function RoleGuard({
  children,
  requiredRole,
  fallback
}: RoleGuardProps) {
  const { session, loading } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (!loading && !session) {
      router.push("/login");
    }
  }, [session, loading, router]);

  if (loading) {
    return <div>Loading...</div>;
  }

  if (!session) {
    return null;
  }

  // Check role from JWT claims (set in jwt callback)
  const userRole = (session as any).role;

  if (userRole !== requiredRole) {
    return fallback || (
      <div>
        <h1>Access Denied</h1>
        <p>You don't have permission to view this page.</p>
      </div>
    );
  }

  return <>{children}</>;
}

// Usage:
// <RoleGuard requiredRole="admin" fallback={<AccessDenied />}>
//   <AdminPanel />
// </RoleGuard>

Refetch on Action

Profile Update with Refetch

Refetch session after profile update

'use client';

import { useAuth } from "@warpy-auth-sdk/core/hooks";
import { useState } from "react";

export default function ProfileForm() {
  const { session, refetch } = useAuth();
  const [name, setName] = useState(session?.user.name || "");
  const [saving, setSaving] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setSaving(true);

    try {
      await fetch("/api/user/update", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name }),
      });

      // Refetch session to get updated user data
      await refetch();

      alert("Profile updated!");
    } catch (error) {
      alert("Failed to update profile");
    } finally {
      setSaving(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        className="border rounded px-3 py-2"
      />
      <button
        type="submit"
        disabled={saving}
        className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:opacity-50"
      >
        {saving ? "Saving..." : "Save"}
      </button>
    </form>
  );
}

TypeScript Support

The hooks include full TypeScript support with type inference:

import { useAuth } from "@warpy-auth-sdk/core/hooks";
import type { Session } from "@warpy-auth-sdk/core";

function MyComponent() {
  const { session, loading } = useAuth();

  // session is typed as Session | null
  // TypeScript knows session.user.email exists
  if (session) {
    const email: string = session.user.email; // ✓ Type-safe
  }
}

Custom Session Type

Extend the session type with custom fields:

import type { Session as BaseSession } from "@warpy-auth-sdk/core";

// Extend base session type
interface CustomSession extends BaseSession {
  role: string;
  subscriptionTier: "free" | "pro" | "enterprise";
}

// Use in component
function MyComponent() {
  const { session } = useAuth();
  const customSession = session as CustomSession | null;

  if (customSession) {
    // TypeScript knows about custom fields
    const role = customSession.role;
    const tier = customSession.subscriptionTier;
  }
}

Best Practices

1. Handle Loading States

Always check the loading state before rendering content:

const { session, loading } = useAuth();

if (loading) {
  return <LoadingSpinner />;
}

// Now safe to use session

2. Protect Routes Client-Side

Redirect unauthenticated users in useEffect:

useEffect(() => {
  if (!loading && !session) {
    router.push("/login");
  }
}, [session, loading, router]);

3. Validate Server-Side

Never trust client-side authentication alone. Always validate server-side:

// Server Component or API Route
const session = await getServerSession();
if (!session) {
  redirect("/login"); // or return 401
}

4. Use AuthProvider at Root

Place AuthProvider as high as possible in your component tree:

// app/layout.tsx
<AuthProvider>
  {children}
</AuthProvider>

5. Minimize Re-renders

Use session data efficiently to avoid unnecessary re-renders:

// Extract only what you need
const { session } = useAuth();
const userName = session?.user.name;

// Instead of passing entire session
<Component name={userName} />

Troubleshooting

Hook Not Working

Ensure AuthProvider is wrapping your component:

// ✗ Wrong
<MyComponent />

// ✓ Correct
<AuthProvider>
  <MyComponent />
</AuthProvider>

Session Not Updating

Call refetch() after actions that update the session:

const { refetch } = useAuth();

await updateUserProfile();
await refetch(); // Refresh session

Infinite Redirect Loop

Check loading state before redirecting:

// ✗ Wrong - infinite loop
useEffect(() => {
  if (!session) {
    router.push("/login");
  }
}, [session]);

// ✓ Correct - check loading
useEffect(() => {
  if (!loading && !session) {
    router.push("/login");
  }
}, [session, loading]);

Next Steps

React Hooks | @warpy-auth-sdk/core