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.
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.
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.
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:
{ __oneRedirect: "/login", __oneRedirectStatus: 302 }REPLACE actionThe protected page never appears. No loader data, no component JavaScript output, no flash of unauthorized content. The server only sends back the redirect path.
On native, loaders are fetched from the dev/production server and evaluated on the device. The same redirect mechanism applies:
__oneRedirect signal after evaluating the loaderreplace navigation to the redirect targetThe component never renders with protected data. This works for both <Link> navigation and programmatic navigation via router.push() / router.navigate().
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} />
}
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).
Loader redirects work best with +ssr routes because the loader runs on every request with access to the request object (cookies, headers, etc.):
| Route Type | Loader Runs | Auth Check |
|---|---|---|
+ssr | Every request | Checks auth per-request via cookies/headers |
+ssg | Once at build time | No per-request auth — only checks build-time state |
+spa | Every request | Same 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.
Loader redirects protect data. When a redirect fires:
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.
| Approach | Best For | Platform |
|---|---|---|
| Loader redirects | SSR data protection, server-side auth | Web + Native |
<Protected /> | Hiding routes from navigation entirely | Web + Native |
<Redirect /> | Client-side redirect in component render | Web + Native |
| Middleware | URL rewrites, API auth, request-level checks | Web (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.