React Router 7 (formerly Remix): This guide covers React Router 7, which is the evolution of the Remix framework.
If you’re migrating from Remix, the integration patterns are very similar.
Quick Start
Integrate Civic Auth into your React Router 7 application using the following steps (a working example is available in our github examples repo):
Important: Make sure your application is using React Router version ^7.0.0 or higher.
This guide assumes you are using TypeScript. Please adjust the snippets as needed to remove the types if you are using
plain JS.
1. Create Auth Route Handlers
Create a catch-all route to handle all authentication-related requests. This route will manage login, logout, callback, and user data endpoints.
Create app/routes/auth.$.tsx:
// Import from the package with the correct path using the index.ts file
// This ensures the debug initialization in the index.ts file is executed
import { createRouteHandlers } from "@civic/auth/react-router-7";
// Create route handlers with app config
const { createAuthLoader, createAuthAction, getUser, getAuthData } = createRouteHandlers({
clientId: "demo-client-1"
});
/**
* Catch-all loader for auth routes (GET requests)
*/
export const loader = createAuthLoader();
/**
* Catch-all action for auth routes (POST requests)
*/
export const action = createAuthAction();
/**
* Export getUser and getAuthData functions for use in other parts of the app
*/
export { getUser, getAuthData };
2. Set Up Root Loader
Configure your root route to provide user data to all routes in your application. This enables server-side rendering with authentication state.
Update app/root.tsx:
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
import type { LinksFunction, LoaderFunction } from "react-router";
import "./tailwind.css";
import { getAuthData } from "~/routes/auth.$";
export const links: LinksFunction = () => [
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
{
rel: "preconnect",
href: "https://fonts.gstatic.com",
crossOrigin: "anonymous",
},
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
},
];
export const loader: LoaderFunction = async ({ request }) => {
const authData = await getAuthData(request);
return {
// We bootstrap our data through the stack
...authData,
};
};
function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export function ErrorBoundary() {
return (
<Layout>
<div className="flex min-h-screen items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900">Something went wrong</h1>
<p className="mt-2 text-gray-600">Please try refreshing the page</p>
</div>
</div>
</Layout>
);
}
export default function App() {
return (
<Layout>
<Outlet />
</Layout>
);
}
Usage
The React Router 7 integration provides several components and hooks for frontend authentication:
The UserButton component provides a complete sign-in/sign-out experience:
import { useLoaderData } from "react-router";
import type { LoaderFunctionArgs, MetaFunction } from "react-router";
import { getUser } from "~/routes/auth.$";
import { UserButton } from "@civic/auth/react-router-7/components/UserButton";
export const meta: MetaFunction = () => {
return [{ title: "My App" }];
};
export async function loader({ request }: LoaderFunctionArgs) {
const user = await getUser(request);
return { user };
}
export default function Index() {
const { user } = useLoaderData<typeof loader>();
return (
<div>
<h1>My App</h1>
<UserButton
onSignIn={(user) => console.log("User signed in", user)}
onSignOut={() => console.log("User signed out")}
/>
{user && (
<div>
<h2>Welcome, {user.name}!</h2>
<p>Email: {user.email}</p>
</div>
)}
</div>
);
}
useUser Hook
For accessing user information in components:
import { useUser } from "@civic/auth/react-router-7";
export function MyComponent() {
const { user, isLoggedIn } = useUser();
if (!isLoggedIn) {
return <div>Please sign in</div>;
}
return <div>Hello {user.name}!</div>;
}
Custom Authentication Logic
For custom sign-in buttons and authentication flows:
import { useCallback } from "react";
import { useUser } from "@civic/auth/react-router-7";
export function CustomSignIn() {
const { signIn, signOut } = useUser();
const doSignIn = useCallback(() => {
console.log("Starting sign-in process");
signIn()
.then(() => {
console.log("Sign-in completed successfully");
})
.catch((error) => {
console.error("Sign-in failed:", error);
});
}, [signIn]);
const handleSignOut = async () => {
await signOut();
};
return (
<div>
<button onClick={doSignIn}>Sign In</button>
<button onClick={handleSignOut}>Sign Out</button>
</div>
);
}
Protected Routes
React Router 7 supports route-level authentication using loaders. Here’s how to create protected routes that require user authentication:
Creating a Protected Route
Create a protected route loader that checks authentication and redirects unauthenticated users:
app/routes/protected._index.tsx
import { data, redirect, Outlet } from "react-router";
import type { LoaderFunctionArgs, MetaFunction } from "react-router";
import { getUser } from "./auth.$";
export const meta: MetaFunction = () => {
return [{ title: "Protected Page - Civic Auth Demo" }];
};
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await getUser(request);
if (!user) {
// Redirect to home page if user is not authenticated
return redirect("/");
}
return data({ user });
};
export default function ProtectedPage() {
return (
<div className="flex h-screen items-center justify-center">
<Outlet />
</div>
);
}
Protected Route Layout
Create the protected route content that displays to authenticated users:
import { Link } from "react-router";
import type { MetaFunction } from "react-router";
import { useUser } from "@civic/auth/react-router-7/useUser";
export const meta: MetaFunction = () => {
return [{ title: "Protected Page - Civic Auth Demo" }];
};
export default function ProtectedLayout() {
const { user } = useUser();
return (
<div className="flex h-screen items-center justify-center text-white">
<div className="flex flex-col items-center gap-8 p-8 border rounded-lg shadow-md">
<h1 className="text-2xl font-bold">Protected Page</h1>
<div className="text-center">
<p className="mb-4">Welcome to this protected page, {user?.name || "User"}!</p>
<p className="text-gray-600 mb-6">This content is only visible to authenticated users.</p>
</div>
<div className="flex gap-4">
<Link to="/" className="px-4 py-2 rounded hover:bg-gray-300">
Back to Home
</Link>
<Link to="/auth/logout" className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700">
Logout
</Link>
</div>
</div>
</div>
);
}
How It Works
- Route Protection: The
protected._index.tsx loader runs on the server before rendering the page
- Authentication Check: It uses
getUser(request) to check if the user is authenticated
- Redirect Logic: If no user is found, it redirects to the home page using
redirect("/")
- User Data: If authenticated, it passes the user data to the route
- Protected Content: The
protected.tsx layout displays content only to authenticated users
Alternative Protection Patterns
You can also create a reusable protection utility:
import { redirect } from "react-router";
import { getUser } from "~/routes/auth.$";
export async function requireAuth(request: Request) {
const user = await getUser(request);
if (!user) {
throw redirect("/auth/login");
}
// Additional admin-specific checks can go here
// Example: Check if user has admin privileges (implement according to your needs)
if (!isUserAdmin(user.email)) {
throw redirect("/");
}
return user;
}
Then use it in any protected route:
import type { LoaderFunctionArgs } from "react-router";
import { requireAuth } from "~/utils/requireAuth";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await requireAuth(request);
return { user };
};
Advanced Configuration
Civic Auth is a “low-code” solution, so most configuration takes place via the dashboard. Changes you make there will be updated automatically in your integration without any code changes.
You can customize the library according to your React Router 7 app’s needs. Configure options when calling createRouteHandlers:
import { createRouteHandlers } from "@civic/auth/react-router-7";
const { createAuthLoader, getUser } = createRouteHandlers({
clientId: "YOUR_CLIENT_ID",
loginSuccessUrl: "/myCustomSuccessEndpoint",
callbackUrl: "/api/myroute/callback",
logoutUrl: "/goodbye",
baseUrl: "https://myapp.com",
});
Configuration Options
| Field | Required | Default | Example | Description |
|---|
clientId | Yes | - | 2cc5633d-2c92-48da-86aa-449634f274b9 | The key obtained on signup to auth.civic.com |
loginSuccessUrl | No | - | /myCustomSuccessEndpoint | In a NextJS app, we will redirect your user to this page once the login is finished. If not set, users will be sent back to the root of your app. |
callbackUrl | No | /auth/callback | /api/myroute/callback | If you cannot host Civic’s SDK handlers in the default location, you can specify a custom callback route here. This is where you must attach Civic’s GET handler as described here, so Civic can complete the OAuth token exchange. Use loginSuccessUrl to redirect after login. |
logoutUrl | No | / | /goodbye | The path your user will be sent to after a successful log-out. |
baseUrl | No | - | https://myapp.com | The public-facing base URL for your application. Required when deploying behind reverse proxies (Cloudfront + Vercel, AWS ALB, nginx, etc.) to ensure authentication redirects use the correct public domain instead of internal origins. |