Shallow routing with window.history
Next.js allows using native window.history.pushState and window.history.replaceState methods to update browser history stack without reloading the page. pushState and replaceState calls integrate into the Next.js Router, allowing sync with usePathname and useSearchParams hooks. This is useful when migrating from strict SPAs like Create React App or Vite that have existing code for shallow routing to update URL state.
Shallow routing example with useSearchParams
In 'use client' component: import useSearchParams from 'next/navigation'; in function, get searchParams with const searchParams = useSearchParams(); create updateSorting function that constructs new URLSearchParams from searchParams.toString(), calls urlSearchParams.set() to set the parameter, then calls window.history.pushState(null, '', `?${urlSearchParams.toString()}`) to update URL without reload.
redirect() must be called outside try/catch blocks
The redirect() function throws an error and should be called outside try/catch blocks, not inside the catch statement.
redirect() accepts absolute URLs for external redirects
The redirect() function accepts absolute URLs and can be used to redirect to external links, not just relative paths.
redirect() function - status codes and context
The redirect() function returns 307 (Temporary Redirect) in most contexts. In Server Actions, it returns 303 (See Other) when JavaScript is unavailable, and uses client-side navigation when JavaScript is available. It can be called in Server Components, Route Handlers, and Server Functions. In Client Components during rendering (not event handlers), redirect() is allowed, but useRouter hook should be used for event handlers instead.
permanentRedirect() function - status code
The permanentRedirect() function returns 308 (Permanent Redirect) in most contexts. In Server Actions, it returns 303 (See Other) when JavaScript is unavailable, and uses client-side navigation when JavaScript is available. It can be called in Server Components, Route Handlers, and Server Functions.
permanentRedirect() accepts absolute URLs for external redirects
The permanentRedirect() function accepts absolute URLs and can be used to redirect to external links.
useRouter hook for client-side navigation
The useRouter hook's push() method can be used to redirect inside event handlers in Client Components. In the App Router, import useRouter from 'next/navigation'. In the Pages Router, import useRouter from 'next/router'. The <Link> component should be preferred when programmatic navigation is not required.
next.config.js redirects() method configuration
The redirects() method in next.config.js returns an array of redirect objects. Each redirect object must have: source (string, the incoming request path), destination (string, the destination path), and permanent (boolean, true for 308 Permanent Redirect, false for 307 Temporary Redirect). The method supports path matching with wildcards (e.g., '/blog/:slug'), and header, cookie, and query matching.
next.config.js redirects() - limit and scale considerations
next.config.js redirects() has a platform-dependent limit. On Vercel, there is a limit of 1,024 redirects. For managing 1000+ redirects, consider creating a custom solution using Proxy instead. The redirects() runs before Proxy in the request lifecycle.
Proxy with NextResponse.redirect() for conditional redirects
Proxy allows you to run code before a request is completed. Using NextResponse.redirect() in Proxy, you can redirect based on conditions (e.g., authentication, session management) or handle large numbers of redirects (1000+). Proxy runs after next.config.js redirects() and before rendering. You can specify custom status codes when calling NextResponse.redirect().
Managing redirects at scale with Proxy and Bloom filter pattern
To manage 1000+ redirects without redeploying, use Proxy with a custom solution. The recommended pattern: (1) Create and store a redirect map in a database (e.g., Vercel Edge Config, Redis) or JSON file with structure {'/old': {'destination': '/new', 'permanent': true}}, (2) Use a Bloom filter to efficiently check if a redirect exists before fetching the actual data, (3) If found in the Bloom filter, forward to a Route Handler/API Route to get the actual redirect entry and return the appropriate status code (308 for permanent, 307 for temporary).
Bloom filter redirect example - redirect data structure
When using a Bloom filter optimization pattern for redirects, the redirect map JSON structure should be: {'/old-path': {'destination': '/new-path', 'permanent': true}}. The status code is determined by the permanent field: 308 if permanent is true, 307 if permanent is false.
Example: redirect() in Server Action after mutation
Example of using redirect() after a database mutation in a Server Action:
```ts
'use server'
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'
export async function createPost(id: string) {
try {
// Call database
} catch (error) {
// Handle errors
}
revalidatePath('/posts') // Update cached posts
redirect(`/post/${id}`) // Navigate to the new post page
}
```
Example: permanentRedirect() in Server Action after URL change
Example of using permanentRedirect() after a mutation that changes an entity's canonical URL:
```ts
'use server'
import { permanentRedirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'
export async function updateUsername(username: string, formData: FormData) {
try {
// Call database
} catch (error) {
// Handle errors
}
revalidateTag('username', 'max') // Update all references to the username
permanentRedirect(`/profile/${username}`) // Navigate to the new user profile
}
```
Example: useRouter in Client Component event handler
Example of using useRouter hook to redirect on button click in a Client Component:
```tsx
'use client'
import { useRouter } from 'next/navigation'
export default function Page() {
const router = useRouter()
return (
<button type="button" onClick={() => router.push('/dashboard')}>
Dashboard
</button>
)
}
```
Example: next.config.js redirects with path matching
Example configuration of redirects in next.config.js with basic and wildcard path matching:
```ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
async redirects() {
return [
// Basic redirect
{
source: '/about',
destination: '/',
permanent: true,
},
// Wildcard path matching
{
source: '/blog/:slug',
destination: '/news/:slug',
permanent: true,
},
]
},
}
export default nextConfig
```
Example: Proxy with authentication-based redirect
Example of using Proxy to redirect unauthenticated users to /login:
```ts
import { NextResponse, NextRequest } from 'next/server'
import { authenticate } from 'auth-provider'
export function proxy(request: NextRequest) {
const isAuthenticated = authenticate(request)
// If the user is authenticated, continue as normal
if (isAuthenticated) {
return NextResponse.next()
}
// Redirect to login page if not authenticated
return NextResponse.redirect(new URL('/login', request.url))
}
export const config = {
matcher: '/dashboard/:path*',
}
```
Example: Proxy with Edge Config for redirect lookup
Example of using Proxy with Vercel Edge Config to look up and execute redirects:
```ts
import { NextResponse, NextRequest } from 'next/server'
import { get } from '@vercel/edge-config'
type RedirectEntry = {
destination: string
permanent: boolean
}
export async function proxy(request: NextRequest) {
const pathname = request.nextUrl.pathname
const redirectData = await get(pathname)
if (redirectData && typeof redirectData === 'string') {
const redirectEntry: RedirectEntry = JSON.parse(redirectData)
const statusCode = redirectEntry.permanent ? 308 : 307
return NextResponse.redirect(redirectEntry.destination, statusCode)
}
// No redirect found, continue without redirecting
return NextResponse.next()
}
```
Example: Proxy with Bloom filter for optimized redirect lookup
Example of using Proxy with a Bloom filter to check for redirects before fetching data:
```ts
import { NextResponse, NextRequest } from 'next/server'
import { ScalableBloomFilter } from 'bloom-filters'
import GeneratedBloomFilter from './redirects/bloom-filter.json'
type RedirectEntry = {
destination: string
permanent: boolean
}
const bloomFilter = ScalableBloomFilter.fromJSON(GeneratedBloomFilter as any)
export async function proxy(request: NextRequest) {
const pathname = request.nextUrl.pathname
if (bloomFilter.has(pathname)) {
const api = new URL(
`/api/redirects?pathname=${encodeURIComponent(request.nextUrl.pathname)}`,
request.nextUrl.origin
)
try {
const redirectData = await fetch(api)
if (redirectData.ok) {
const redirectEntry: RedirectEntry | undefined =
await redirectData.json()
if (redirectEntry) {
const statusCode = redirectEntry.permanent ? 308 : 307
return NextResponse.redirect(redirectEntry.destination, statusCode)
}
}
} catch (error) {
console.error(error)
}
}
return NextResponse.next()
}
```
Example: Route Handler for Bloom filter redirect lookup
Example Route Handler that returns redirect entries for the Bloom filter pattern:
```ts
import { NextRequest, NextResponse } from 'next/server'
import redirects from '@/app/redirects/redirects.json'
type RedirectEntry = {
destination: string
permanent: boolean
}
export function GET(request: NextRequest) {
const pathname = request.nextUrl.searchParams.get('pathname')
if (!pathname) {
return new Response('Bad Request', { status: 400 })
}
const redirect = (redirects as Record<string, RedirectEntry>)[pathname]
if (!redirect) {
return new Response('No redirect', { status: 400 })
}
return NextResponse.json(redirect)
}
```
Proxy request lifecycle position
Proxy executes after next.config.js redirects() and before rendering. This means that redirects configured in next.config.js are processed first, then Proxy runs, then the request is rendered.