Next.js

Next.js Redirect from / to another page

If your redirect works only after render, flickers, or fires in the wrong place, you are probably using the wrong redirect API for the job. Here is the exact mapping.

Mahdi Ben RhoumaMahdi Ben Rhouma
Updated
6 min read

The symptom is usually one of these:

  • the home page renders and then jumps away
  • auth redirects flicker
  • redirect() works in one file but not another
  • a client-side button redirect gets copy-pasted into server code

The root cause is that Next.js has five redirect tools, and each one is for a different phase of the request.

The shortest correct answer is:

  • use redirect() for request-time server redirects
  • use useRouter().push() in a client event handler
  • use redirects() in next.config.js for static path rules

If / should always redirect, use next.config.js#

This is the cleanest answer when the rule is fixed.

ts
// next.config.mjs
const nextConfig = {
  async redirects() {
    return [
      {
        source: '/',
        destination: '/dashboard',
        permanent: false,
      },
    ]
  },
}
 
export default nextConfig

This happens before rendering, so there is no flash of the home page first.

If / redirects based on auth or data, use redirect()#

In the App Router, this is the server-side pattern:

tsx
// app/page.tsx
import { redirect } from 'next/navigation'
 
export default async function HomePage() {
  const isLoggedIn = false
 
  if (isLoggedIn) {
    redirect('/dashboard')
  }
 
  redirect('/login')
}

The current Next.js docs say redirect() returns a 307 by default, and returns 303 when used in a Server Action.

If the redirect happens after a button click, use useRouter()#

This is for event handlers in client components, not for server rendering.

tsx
'use client'
 
import { useRouter } from 'next/navigation'
 
export default function GoToDashboardButton() {
  const router = useRouter()
 
  return (
    <button type="button" onClick={() => router.push('/dashboard')}>
      Dashboard
    </button>
  )
}

If you do not need imperative navigation, keep using <Link />.

The mistake that causes most bad redirects#

People often use the client pattern for a server problem:

tsx
// Avoid this for root-route redirect logic
'use client'
 
import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
 
export default function Page() {
  const router = useRouter()
 
  useEffect(() => {
    router.push('/dashboard')
  }, [router])
 
  return null
}

This works, but it is the wrong tool for a route-level redirect. The user briefly loads /, then navigates away. Search crawlers and users both get a worse experience.

Another easy mistake: calling redirect() inside try#

The docs explicitly note that redirect() throws. If you wrap it inside try/catch, you can accidentally swallow the navigation. In a Route Handler the same mistake shows up as Error: NEXT_REDIRECT in your logs and a redirect to the wrong place, or a 500; the redirect() caught inside a Route Handler page reproduces each case and shows the fix.

Use this shape instead:

ts
import { redirect } from 'next/navigation'
 
export async function createProject() {
  let id: string
 
  try {
    id = 'abc123'
  } catch (error) {
    throw error
  }
 
  redirect(`/projects/${id}`)
}

Which redirect API should you pick?#

  • Known path rule for every request: redirects() in next.config.js
  • Request-time condition in server code: redirect()
  • Canonical URL changed permanently: permanentRedirect()
  • Click handler in client UI: useRouter().push()

That mapping is the part most articles skip, and it is why the same redirect can feel broken in three different ways.

For adjacent routing gotchas, keep these open:

References#

Redirecting / conditionally, before the page ever renders#

The three tools above cover a fixed rule (next.config.js), a server-side condition inside a Server Component or Server Action (redirect()), and a client click handler (useRouter().push()). There's a fourth case none of them fit well: a condition you need to check on every request to / — a cookie, a device hint, an A/B bucket — where you don't want the home page's Server Component to start rendering first.

That's what Middleware is for. It runs at the edge, before routing decides which page to render, and it can inspect cookies and headers directly:

ts
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  const hasSession = request.cookies.has('session')
 
  if (request.nextUrl.pathname === '/' && !hasSession) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
}
 
export const config = {
  matcher: '/',
}

The trade-off versus redirect() inside app/page.tsx: Middleware never lets the root page's Server Component start executing at all for a redirected request, which is cheaper when the page does real data fetching you'd rather skip entirely. The trade-off versus next.config.js redirects(): Middleware can read cookies and headers per request, while redirects() can only match on the URL itself.

The most common way this quietly fails isn't a bad condition — it's the matcher config not matching the route you expect, so the redirect never fires and you're left debugging the wrong function. That specific failure mode, and how to confirm whether Middleware is even running for a given path, is covered in Next.js Middleware Not Running in Vercel Production.

Why the redirect silently drops your query string#

A redirect from /?ref=partner-x to /dashboard sometimes keeps ?ref=partner-x and sometimes doesn't, depending on which of the three tools above did the redirecting — and that inconsistency is a common source of lost attribution data.

With redirects() in next.config.js, query parameters on the incoming request that aren't already captured by a named path segment in source are appended automatically to whatever destination you configured — you don't have to do anything to preserve them, and you don't need to name them in source unless you want to read their values there.

With redirect() from next/navigation, there's no such passthrough, because you're constructing the destination string yourself. If the incoming request had a query string you want to carry forward, you have to read it and append it explicitly:

tsx
import { redirect } from 'next/navigation'
 
export default async function HomePage({
  searchParams,
}: {
  searchParams: Promise<{ ref?: string }>
}) {
  const { ref } = await searchParams
  redirect(ref ? `/dashboard?ref=${encodeURIComponent(ref)}` : '/dashboard')
}

Skip this and any campaign or referral parameter that landed on / is gone the moment the redirect fires — the destination page never receives it. For the general mechanics of reading searchParams in the App Router (Server Components get a promise, Client Components get a hook), see How to get query string parameters in Next.js.

Further reading: Fix: Next.js middleware not triggering. Also see Fix: useRouter query undefined on first render.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.