Loader Redirects

Loader redirects let you protect routes server-side on both web and native. When a loader calls redirect(), unauthorized users are redirected before any sensitive data or page content reaches the client — for direct page loads, client-side <Link> navigation, and React Native navigation.

Basic Usage

Use the redirect() helper in a loader to guard a route. Use +ssr so the loader runs on every request:

app/dashboard+ssr.tsx

import { redirect, useLoader } from 'one'
import type { LoaderProps } from 'one'
export async function loader({ request }: LoaderProps) {
const session = await getSession(request)
if (!session) {
throw redirect('/login')
}
return { user: session.user }
}
export default function Dashboard() {
const { user } = useLoader(loader)
return <Text>Welcome, {user.name}</Text>
}

Both throw redirect() and return redirect() work. throw is often cleaner since it stops execution immediately.

How It Works

Direct Page Load

When a user navigates directly to a protected URL (typing it in, refreshing, or following an external link), the loader runs on the server. If it calls redirect(), the server returns a standard HTTP 302 response and the browser redirects to the target page. The protected page never renders.

Client-Side Navigation

When a user clicks a <Link> to a protected route, the behavior is more nuanced. Loaders run on the server, and the client fetches the result as a JavaScript module. Without special handling, the browser’s module loader would silently follow a raw 302, try to parse the HTML login page as JavaScript, and fail.

One handles this differently:

  1. The server detects the redirect response from the loader
  2. Instead of returning a raw HTTP 302, it transforms the response into a JS module containing only redirect metadata — something like { __oneRedirect: "/login", __oneRedirectStatus: 302 }
  3. The client receives this module and detects the redirect signal before navigating or rendering anything
  4. The client navigates to the redirect target using a REPLACE action

The protected page never appears. No loader data, no component JavaScript output, no flash of unauthorized content. The server only sends back the redirect path.

React Native

On native, loaders are fetched from the dev/production server and evaluated on the device. The same redirect mechanism applies:

  1. The native app fetches the loader JavaScript from the server
  2. The server runs the loader, detects the redirect, and transforms it into a module with redirect metadata
  3. The native client detects the __oneRedirect signal after evaluating the loader
  4. The router performs a replace navigation to the redirect target

The component never renders with protected data. This works for both <Link> navigation and programmatic navigation via router.push() / router.navigate().

Protecting Multiple Routes

Use a shared auth helper to keep loaders clean:

features/auth/server.ts

import { redirect } from 'one'
import type { LoaderProps } from 'one'
export async function requireAuth(request: LoaderProps['request']) {
const session = await getSession(request)
if (!session) {
throw redirect('/login')
}
return session
}

app/dashboard+ssr.tsx

import { useLoader } from 'one'
import { requireAuth } from '~/features/auth/server'
export async function loader({ request }) {
const session = await requireAuth(request)
return { user: session.user }
}
export default function Dashboard() {
const { user } = useLoader(loader)
return <Text>Welcome, {user.name}</Text>
}

app/settings+ssr.tsx

import { useLoader } from 'one'
import { requireAuth } from '~/features/auth/server'
export async function loader({ request }) {
const session = await requireAuth(request)
const prefs = await getUserPreferences(session.userId)
return { prefs }
}
export default function Settings() {
const { prefs } = useLoader(loader)
return <PreferencesForm defaults={prefs} />
}

Redirect Status Codes

The redirect() helper accepts an optional status code:

// 302 (default) - temporary redirect
throw redirect('/login')
// 301 - permanent redirect
throw redirect('/new-path', 301)
// 307 - temporary redirect, preserves HTTP method
throw redirect('/login', 307)

For auth guards, the default 302 is usually what you want. Use 307 if you need to preserve the original HTTP method (e.g., a POST request should remain a POST after redirect).

SSR vs SSG Routes

Loader redirects work best with +ssr routes because the loader runs on every request with access to the request object (cookies, headers, etc.):

Route TypeLoader RunsAuth Check
+ssrEvery requestChecks auth per-request via cookies/headers
+ssgOnce at build timeNo per-request auth — only checks build-time state
+spaEvery requestSame as SSR for loader execution

For protected routes, always use +ssr so auth is checked on every navigation.

When an SSG route redirects at build time, One generates a static redirect file. If a runtime server is present in production, it re-evaluates the loader per-request — so authenticated users see the real page while unauthenticated users get redirected.

What Gets Protected

Loader redirects protect data. When a redirect fires:

  • The loader data is never sent to the client
  • The page component never renders on the server for that request
  • The client receives only the redirect target path

Component JavaScript is always downloadable (this is true in every framework). Loaders protect the sensitive data your components would display — user profiles, account details, admin data, etc.

Loader Redirects vs Other Approaches

ApproachBest ForPlatform
Loader redirectsSSR data protection, server-side authWeb + Native
<Protected />Hiding routes from navigation entirelyWeb + Native
<Redirect />Client-side redirect in component renderWeb + Native
MiddlewareURL rewrites, API auth, request-level checksWeb (server)

You can combine these approaches. For example, use loader redirects to protect data on SSR routes, and <Protected /> to hide those routes from the tab bar on native.

Edit this page on GitHub.