Integrate Sessions With The Frontend (React)

Create a React Context to store the user's session state and protect authenticated routes.

Now that you've updated your server endpoints to manage the user's session, we'll next go over how to store that session data into your frontend using the React Client Auth SDK. We'll also show how to use the authenticated state from the SDK's React Context to protect routes that require authentication.


Install the React SDK

Install the React Client Auth SDK using your preferred package manager CLI:

npm install @wristband/react-client-auth
yarn add @wristband/react-client-auth
pnpm add @wristband/react-client-auth

Configure the SDK and Propagate Authenticated State

The SDK contains a WristbandAuthProvider component that establishes and manages authentication state throughout your React frontend. It will call the Session Endpoint that was implemented earlier to retrieve the user's session data. If the call to the Session Endpoint succeeds, we can be sure that the user has successfully authenticated, and any pertinent session data will be stored in the Provider's React Context. However, if the call to the Session Endpoint returns a 401 status code, then we know that the user does not have a valid session, and the user will be redirected to the Login Endpoint.

WristbandAuthProvider requires three URL endpoints:

  • loginUrl: The URL of your backend server's Login Endpoint.
  • logoutUrl: The URL of your backend server's Logout Endpoint.
  • sessionUrl: The URL of your backend server's Session Endpoint.

Place the WristbandAuthProvider at your app's root to ensure the user's authenticated state is available throughout your application and verified on initial load. Configure the component with the 3 URLs above.

//
// Location -> src/pages/_app.tsx
//
import type { AppProps } from 'next/app';
import { WristbandAuthProvider } from '@wristband/react-client-auth';
import Layout from '@/components/root-layout';

import '@/styles/globals.css';

/**
 * We place the WristbandAuthProvider at the root of our application in _app.tsx 
 * because this ensures that authentication state is globally available to all 
 * pages and components. This approach allows any component in the application 
 * to access auth context methods and the current user state. Since _app.tsx wraps
 * all pages in NextJS Page Router, this creates a single source of truth for
 * authentication that persists across page navigations.
 */
export default function App({ Component, pageProps }: AppProps) {
  return (
    <WristbandAuthProvider
      loginUrl="/api/auth/login"
      logoutUrl="/api/auth/logout"
      sessionUrl="/api/v1/session"
    >
      <Layout>
        <Component {...pageProps} />
      </Layout>
    </WristbandAuthProvider>
  );
}

//
// #01) Create the wrapper client component for WristbandAuthProvider
// Location -> src/context/wristband-auth-provider-wrapper.tsx
//
'use client';

import { ReactNode } from 'react';
import { WristbandAuthProvider } from '@wristband/react-client-auth';

/**
 * In the App Router, React Context providers must be client components (marked with
 * 'use client') since they rely on React's Context API which requires client-side
 * functionality. By creating this dedicated wrapper component, we isolate the
 * client-side code to just this file, keeping our root layout as a server component
 * for better performance. This pattern is the recommended approach for using third-party
 * context providers with NextJS App Router while maintaining the benefits of server
 * components elsewhere in the application.
 */
export function WristbandAuthProviderWrapper({ children }: { children: ReactNode }) {
  return (
    <WristbandAuthProvider
      loginUrl="/api/auth/login"
      logoutUrl="/api/auth/logout"
      sessionUrl="/api/v1/session"
    >
      {children}
    </WristbandAuthProvider>
  );
}

// ================================================= //

//
// #02) Use the wrapper client component in the root layout
// Location -> src/app/layout.tsx
//
import type { Metadata } from 'next';
import { WristbandAuthProviderWrapper } from '@/context/wristband-auth-provider-wrapper';

import './globals.css';

/**
 * We place the WristbandAuthProviderWrapper at the root layout level to ensure
 * authentication state is available throughout the entire application. This creates a
 * single source of truth for auth data that persists across navigation.  While server
 * components cannot directly use hooks like useWristbandAuth(), any client components
 * within the tree can access this context.  Additionally, server components can pass
 * authentication data received from API calls to client components that need it, allowing
 * for a hybrid approach to authentication state management.
 */
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="en">
      <body>
        <WristbandAuthProviderWrapper>
          {children}
        </WristbandAuthProviderWrapper>
      </body>
    </html>
  );
}




Protect Frontend Routes and Components

The SDK exposes some hooks and utility functions that enable common patterns like conditional rendering of authenticated/unauthenticated views, route protection, or dynamic UI updates based on the user's auth status.

Hooks

  • useWristbandAuth(): Get access to the user's authentication state throughout your app.
  • useWristbandSession(): Get access to the user data provided by your backend server's Session Endpoint.

Utility Functions

  • redirectToLogin(): Redirects the user to your backend server's Login Endpoint.
  • redirectToLogout(): Redirects the user to your backend server's Logout Endpoint.

Below are several common patterns you can apply as needed.


Use The User's Authenticated State to Render Different Components

//
// Location -> src/pages/index.tsx
//
import { useWristbandAuth, useWristbandSession, redirectToLogin, redirectToLogout } from '@wristband/react-client-auth';

export default function HomePage() {
  const { isAuthenticated, isLoading } = useWristbandAuth();
  const { userId, tenantId } = useWristbandSession();
  
  if (isLoading) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      {isAuthenticated && (
        <>
          <h1>Welcome to Wristband Auth</h1>
          <p>Your User ID: {userId}</p>
          <p>Your Tenant ID: {tenantId}</p>
          <button onClick={() => redirectToLogout('/api/auth/logout')}>Logout</button>
        </>
      )}
      {!isAuthenticated && (
        <>
          <h1>Welcome to Wristband Auth</h1>
          <button onClick={() => redirectToLogin('/api/auth/login')}>Login</button>
        </>
      )}
    </div>
  );
}

//
// Location -> src/app/page.tsx
//
'use client';

import { useWristbandAuth, useWristbandSession, redirectToLogin, redirectToLogout } from '@wristband/react-client-auth';

/**
 * In NextJS App Router, React hooks like useWristbandAuth() and useWristbandSession() can
 * only be used in client components, not in server components. This is a fundamental
 * constraint of React Server Components. Therefore, the component using those hooks must
 * be marked with 'use client'. If you need authentication data in server components, you'll
 * need to fetch it directly from the Session rather than using these hooks.
 */
export default function HomePage() {
  const { isAuthenticated, isLoading } = useWristbandAuth();
  const { userId, tenantId } = useWristbandSession();

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

  return (
    <div>
      {isAuthenticated && (
        <>
          <h1>Welcome to Wristband Auth</h1>
          <p>Your User ID: {userId}</p>
          <p>Your Tenant ID: {tenantId}</p>
          <button onClick={() => redirectToLogout('/api/auth/logout')}>Logout</button>
        </>
      )}
      {!isAuthenticated && (
        <>
          <h1>Welcome to Wristband Auth</h1>
          <button onClick={() => redirectToLogin('/api/auth/login')}>Login</button>
        </>
      )}
    </div>
  );
}


Create an AuthGuard Component to Protect Explicit Routes

//
// #01) Create an AuthGuard component
// Location -> src/components/auth-guard.tsx
//
import { useRouter } from 'next/router';
import { useWristbandAuth } from '@wristband/react-client-auth';
import { ReactNode } from 'react';

export default function AuthGuard({ children }: { children: ReactNode }) {
  const { isAuthenticated, isLoading } = useWristbandAuth();
  const router = useRouter();
  
  if (isLoading) {
    return <div>Loading...</div>;
  }
  
  if (!isAuthenticated) {
    router.replace('/login');
    return null;
  }
  
  return <>{children}</>;
}

// ================================================= //

//
// #02) Use the AuthGuard component in your pages and components
// Location -> src/pages/dashboard.tsx
//
import AuthGuard from '@/components/auth-guard';

export default function Dashboard() {
  return (
    <AuthGuard>
      <div>Dashboard Content</div>
    </AuthGuard>
  );
}

//
// #01) Create an AuthGuard component
// Location -> src/components/auth-guard.tsx
//
'use client';

import { useRouter } from 'next/navigation';
import { useWristbandAuth } from '@wristband/react-client-auth';
import { ReactNode } from 'react';

/**
 * In NextJS App Router, React hooks like useWristbandAuth() and useWristbandSession() can
 * only be used in client components, not in server components. This is a fundamental
 * constraint of React Server Components. Therefore, the component using those hooks must
 * be marked with 'use client'. If you need authentication data in server components, you'll
 * need to fetch it directly from the Session rather than using these hooks.
 */
export default function AuthGuard({ children }: { children: ReactNode }) {
  const { isAuthenticated, isLoading } = useWristbandAuth();
  const router = useRouter();
  
  if (isLoading) {
    return <div>Loading...</div>;
  }
  
  if (!isAuthenticated) {
    router.replace('/login');
    return null;
  }
  
  return <>{children}</>;
}

// ================================================= //

//
// #02) Use the AuthGuard component in your pages and components.
// Location -> src/app/dashboard/page.tsx
//
import AuthGuard from '@/components/auth-guard';

export default function Dashboard() {
  return (
    <AuthGuard>
      <div>Dashboard Content</div>
    </AuthGuard>
  );
}


Use Session Directly During Server Processing

//
// #01) Create an AuthGuard component
// Location -> src/pages/protected.tsx
//
import { GetServerSideProps } from 'next';
import { getSession } from '@/session/session';

export default function ProtectedPage({ tenantId }) {
  return (
    <div>
      <h1>Protected Page</h1>
      <p>Your tenantId is {tenantId}!</p>
    </div>
  );
}

/**
 * This pattern uses getServerSideProps() and getSession() to protect routes
 * at the server level before rendering. It checks authentication during SSR,
 * enables data fetching with the user's session, and prevents unauthorized
 * users from seeing protected content.
 *
 * IMPORTANT: While effective, NextJS middleware (covered in later sections of this guide)
 * is generally preferred for route protection since it runs before matching routes,
 * offers better performance, works with both static and dynamic routes, and is
 * the recommended approach in both Page Router and App Router.
 */
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
  // Get the session during server-side rendering
  const session = await getSession(req, res);
  const { isAuthenticated, tenantId } = session;
  
  // Check if user is authenticated
  if (!session.isAuthenticated) {
    // Redirect to login if not authenticated
    return {
      redirect: { destination: '/api/auth/login', permanent: false },
    };
  }
  
  // If authenticated, pass data to the page component.
  return {
    props: { tenantId },
  };
};

//
// Location -> src/app/protected/page.tsx
//
import { redirect } from 'next/navigation';
import { getSession } from '@/session/session';

/**
 * This pattern leverages React Server Components in the App Router to check
 * authentication status before rendering protected content. Server components
 * execute entirely on the server, allowing direct session verification and
 * protected data fetching in a single operation.
 * 
 * IMPORTANT: Server Component authentication approach only evaluates when the
 * page is initially loaded or revalidated. If a user's session expires while
 * viewing the page, they'll continue to see protected content until navigation
 * or refresh. NextJS middleware (covered in later sections of this guide)
 * is generally preferred for route protection since it runs before matching routes,
 * offers better performance, works with both static and dynamic routes, and is
 * the recommended approach in both Page Router and App Router.
 */
export default async function ProtectedPage() {
  // Use your getSession function directly.
  const session = await getSession();
  const { isAuthenticated, tenantId } = session;
  
  // Check authentication and redirect if needed.
  if (!isAuthenticated) {
    redirect('/login');
  }
  
  // Only authenticated users will see this content.
  return (
    <div>
      <h1>Protected Page</h1>
      <p>Your tenantId is {tenantId}!</p>
    </div>
  );
}


Congratulations on making it this far! You've now learned how to integrate Wristband authentication into both your NextJS server and React frontend!


What’s Next

Next let's test the authentication flows to ensure that sessions are properly being managed.