Pages, Routing and Navigation in NextJS

In Next.js, a page is a React Component exported from a file (.js, .jsx, .ts, or .tsx) in the pages directory. The component can have any name, but you must export it as a default export.

pages/about.js
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
export default function About() {
return <div>About</div>
}
export default function About() { return <div>About</div> }
export default function About() {
  return <div>About</div>
}

    Pages for static routes

    • pages/index.js → maps to / route
    • pages/posts/first-post.js → maps to /posts/first-post

    Pages for dynamic routes

    Sometimes routes have dynamic segments, for example if the route uses a blog title that is read from the database and the title is supposed to be used in the page. In that case you have to use bracket-syntax:

    • pages/blog/[slug].js/blog/:slug (e.g. /blog/hello-world)
    • pages/[username]/settings.js/:username/settings (e.g. /foo/settings)
    • pages/post/[...all].js/post/* (e.g. /post/2020/id/title)

    Catch-All Routes

    Dynamic routes can be extended to catch all paths by adding three dots (...) inside the brackets: pages/post/[...slug].js matches /post/a, but also /post/a/b, /post/a/b/c and so on. The resulting query object would be:

    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    // the file pages/post/[...slug].js
    // if called as /post/a would result as query:
    { "slug": ["a"] }
    // if called as /post/a/b would result as query:
    { "slug": ["a", "b"] }
    // the file pages/post/[...slug].js // if called as /post/a would result as query: { "slug": ["a"] } // if called as /post/a/b would result as query: { "slug": ["a", "b"] }
    // the file pages/post/[...slug].js
    
    // if called as /post/a would result as query:
    { "slug": ["a"] }
    
    // if called as /post/a/b would result as query:
    { "slug": ["a", "b"] }

    Catch-All Routes that are optional

    Catch all routes can be made optional by including the parameter in double brackets ([[...slug]]).

    For example, pages/post/[[...slug]].js will match /post, /post/a, /post/a/b, and so on.

    The main difference between catch all and optional catch all routes is that with optional, the route without the parameter is also matched. The query objects are as follows:

    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    // GET `/post` (empty object)
    { }
    // `GET /post/a` (single-element array)
    { "slug": ["a"] }
    // `GET /post/a/b` (multi-element array)
    { "slug": ["a", "b"] }
    // GET `/post` (empty object) { } // `GET /post/a` (single-element array) { "slug": ["a"] } // `GET /post/a/b` (multi-element array) { "slug": ["a", "b"] }
    // GET `/post` (empty object)
    { } 
    
    // `GET /post/a` (single-element array)
    { "slug": ["a"] }
    
    // `GET /post/a/b` (multi-element array)
    { "slug": ["a", "b"] }

    Note: Predefined routes take precedence over dynamic routes, and dynamic routes over catch a

    Metadata in <Head>

    You change metadata by using <Head> Component instead of <head> element.

    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    import Head from 'next/head'
    export default function FirstPost() {
    return (
    <>
    <Head>
    <title>First Post</title>
    </Head>
    <h1>First Post</h1>
    </>
    )
    }
    import Head from 'next/head' export default function FirstPost() { return ( <> <Head> <title>First Post</title> </Head> <h1>First Post</h1> </> ) }
    import Head from 'next/head'
    
    export default function FirstPost() {
      return (
        <>
          <Head>
            <title>First Post</title>
          </Head>
          <h1>First Post</h1>
        </>
      )
    }

    Linking to static routes

    Any <Link /> in the viewport (initially or through scroll) will be prefetched by default (including the corresponding data) for pages using Static Generation. The corresponding data for server-rendered routes is fetched only when the <Link /> is clicked.

    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    import Link from 'next/link'
    export default function FirstPost() {
    return (
    <>
    <Link href="/">Home</Link>
    </>
    )
    }
    import Link from 'next/link' export default function FirstPost() { return ( <> <Link href="/">Home</Link> </> ) }
    import Link from 'next/link'
    
    export default function FirstPost() {
      return (
        <>
          <Link href="/">Home</Link>
        </>
      )
    }

    Linking to dynamic routes

    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    import Link from 'next/link'
    function Posts({ posts }) {
    return (
    <ul>
    {posts.map((post) => (
    <li key={post.id}>
    <Link href={`/blog/${encodeURIComponent(post.slug)}`}>
    <a>{post.title}</a>
    </Link>
    </li>
    ))}
    </ul>
    )
    }
    export default Posts
    import Link from 'next/link' function Posts({ posts }) { return ( <ul> {posts.map((post) => ( <li key={post.id}> <Link href={`/blog/${encodeURIComponent(post.slug)}`}> <a>{post.title}</a> </Link> </li> ))} </ul> ) } export default Posts
    import Link from 'next/link'
    
    function Posts({ posts }) {
      return (
        <ul>
          {posts.map((post) => (
            <li key={post.id}>
              <Link href={`/blog/${encodeURIComponent(post.slug)}`}>
                <a>{post.title}</a>
              </Link>
            </li>
          ))}
        </ul>
      )
    }
    
    export default Posts

    Or alternatively use a URL object:

    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    <Link
    href={{
    pathname: '/blog/[slug]',
    query: { slug: post.slug },
    }}
    >
    <Link href={{ pathname: '/blog/[slug]', query: { slug: post.slug }, }} >
    <Link
      href={{
        pathname: '/blog/[slug]',
        query: { slug: post.slug },
      }}
    >

    If you need to link to an external page outside the NextJS app, just use an <a> tag without Link. If you need to add attributes like, for example, className, add it to an a tag within the Link element, not to the Link tag itself.

    Linking imperatively

    <Link> should be able to cover most of your routing needs, but you can also do client-side navigation imperatively:

    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    import { useRouter } from 'next/router'
    function ReadMore() {
    const router = useRouter()
    return (
    <span onClick={() => router.push('/about')}>Click here to read more</span>
    )
    }
    export default ReadMore
    import { useRouter } from 'next/router' function ReadMore() { const router = useRouter() return ( <span onClick={() => router.push('/about')}>Click here to read more</span> ) } export default ReadMore
    import { useRouter } from 'next/router'
    
    function ReadMore() {
      const router = useRouter()
    
      return (
        <span onClick={() => router.push('/about')}>Click here to read more</span>
      )
    }
    
    export default ReadMore
    

    Change URL without fetching data again

    Also called Shallow Routing. getServerSideProps, getStaticProps, and getInitialProps will not be called, but you will still receive the updated pathname and the query via the router object when you set the shallow option to true:

    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    import { useEffect } from 'react'
    import { useRouter } from 'next/router'
    // Current URL is '/'
    function Page() {
    const router = useRouter()
    useEffect(() => {
    // Always do navigations after the first render
    router.push('/?counter=10', undefined, { shallow: true })
    }, [])
    useEffect(() => {
    // The counter changed!
    }, [router.query.counter])
    }
    export default Page
    import { useEffect } from 'react' import { useRouter } from 'next/router' // Current URL is '/' function Page() { const router = useRouter() useEffect(() => { // Always do navigations after the first render router.push('/?counter=10', undefined, { shallow: true }) }, []) useEffect(() => { // The counter changed! }, [router.query.counter]) } export default Page
    import { useEffect } from 'react'
    import { useRouter } from 'next/router'
    
    // Current URL is '/'
    function Page() {
      const router = useRouter()
    
      useEffect(() => {
        // Always do navigations after the first render
        router.push('/?counter=10', undefined, { shallow: true })
      }, [])
    
      useEffect(() => {
        // The counter changed!
      }, [router.query.counter])
    }
    
    export default Page

    Shallow routing only works for same-page URL changes.

    next/router

    Introducing the useRouter hook:

    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    import { useRouter } from 'next/router'
    function ActiveLink({ children, href }) {
    const router = useRouter()
    const style = {
    marginRight: 10,
    color: router.asPath === href ? 'red' : 'black',
    }
    const handleClick = (e) => {
    e.preventDefault()
    router.push(href)
    }
    return (
    <a href={href} onClick={handleClick} style={style}>
    {children}
    </a>
    )
    }
    export default ActiveLink
    import { useRouter } from 'next/router' function ActiveLink({ children, href }) { const router = useRouter() const style = { marginRight: 10, color: router.asPath === href ? 'red' : 'black', } const handleClick = (e) => { e.preventDefault() router.push(href) } return ( <a href={href} onClick={handleClick} style={style}> {children} </a> ) } export default ActiveLink
    import { useRouter } from 'next/router'
    
    function ActiveLink({ children, href }) {
      const router = useRouter()
      const style = {
        marginRight: 10,
        color: router.asPath === href ? 'red' : 'black',
      }
    
      const handleClick = (e) => {
        e.preventDefault()
        router.push(href)
      }
    
      return (
        <a href={href} onClick={handleClick} style={style}>
          {children}
        </a>
      )
    }
    
    export default ActiveLink

    The router object returned by useRouter:

    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    // Current route. That is the path of the page in /pages
    pathname: String
    // The query string parsed to an object. It will be an empty object during prerendering if the page doesn't have data fetching requirements. Defaults to {}
    query: Object
    // The path (including the query) shown in the browser without the configured basePath or locale.
    asPath: String
    // Whether the current page is in fallback mode.
    isFallback: boolean
    // The active basePath (if enabled).
    basePath: String
    // The active locale (if enabled)
    locale: String
    // All supported locales (if enabled).
    locales: String[]
    // The current default locale (if enabled)
    defaultLocale: String
    // Whether the router fields are updated client-side and ready for use. Should only be used inside of useEffect methods and not for conditionally rendering on the server.
    isReady: boolean
    // Whether the application is currently in preview mode.
    isPreview: boolean
    // Current route. That is the path of the page in /pages pathname: String // The query string parsed to an object. It will be an empty object during prerendering if the page doesn't have data fetching requirements. Defaults to {} query: Object // The path (including the query) shown in the browser without the configured basePath or locale. asPath: String // Whether the current page is in fallback mode. isFallback: boolean // The active basePath (if enabled). basePath: String // The active locale (if enabled) locale: String // All supported locales (if enabled). locales: String[] // The current default locale (if enabled) defaultLocale: String // Whether the router fields are updated client-side and ready for use. Should only be used inside of useEffect methods and not for conditionally rendering on the server. isReady: boolean // Whether the application is currently in preview mode. isPreview: boolean
    // Current route. That is the path of the page in /pages
    pathname: String
    
    // The query string parsed to an object. It will be an empty object during prerendering if the page doesn't have data fetching requirements. Defaults to {}
    query: Object
    
    // The path (including the query) shown in the browser without the configured basePath or locale.
    asPath: String
    
    // Whether the current page is in fallback mode.
    isFallback: boolean
    
    // The active basePath (if enabled).
    basePath: String
    
    // The active locale (if enabled)
    locale: String
    
    // All supported locales (if enabled).
    locales: String[]
    
    // The current default locale (if enabled)
    defaultLocale: String
    
    // Whether the router fields are updated client-side and ready for use. Should only be used inside of useEffect methods and not for conditionally rendering on the server.
    isReady: boolean
    
    // Whether the application is currently in preview mode.
    isPreview: boolean

    Push

    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    import { useEffect } from 'react'
    import { useRouter } from 'next/router'
    // Here you would fetch and return the user
    const useUser = () => ({ user: null, loading: false })
    export default function Page() {
    const { user, loading } = useUser()
    const router = useRouter()
    useEffect(() => {
    if (!(user || loading)) {
    router.push('/login')
    }
    }, [user, loading])
    return <p>Redirecting...</p>
    }
    import { useEffect } from 'react' import { useRouter } from 'next/router' // Here you would fetch and return the user const useUser = () => ({ user: null, loading: false }) export default function Page() { const { user, loading } = useUser() const router = useRouter() useEffect(() => { if (!(user || loading)) { router.push('/login') } }, [user, loading]) return <p>Redirecting...</p> }
    import { useEffect } from 'react'
    import { useRouter } from 'next/router'
    
    // Here you would fetch and return the user
    const useUser = () => ({ user: null, loading: false })
    
    export default function Page() {
      const { user, loading } = useUser()
      const router = useRouter()
    
      useEffect(() => {
        if (!(user || loading)) {
          router.push('/login')
        }
      }, [user, loading])
    
      return <p>Redirecting...</p>
    }
    

    Replace

    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    import { useRouter } from 'next/router'
    export default function Page() {
    const router = useRouter()
    return <button onClick={() => router.replace('/home')}>Click me</button>
    }
    import { useRouter } from 'next/router' export default function Page() { const router = useRouter() return <button onClick={() => router.replace('/home')}>Click me</button> }
    import { useRouter } from 'next/router'
    
    export default function Page() {
      const router = useRouter()
    
      return <button onClick={() => router.replace('/home')}>Click me</button>
    }

    beforePopState

    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    import { useEffect } from 'react'
    import { useRouter } from 'next/router'
    export default function Page() {
    const router = useRouter()
    useEffect(() => {
    router.beforePopState(({ url, as, options }) => {
    // I only want to allow these two routes!
    if (as !== '/' && as !== '/other') {
    // Have SSR render bad routes as a 404.
    window.location.href = as
    return false
    }
    return true
    })
    }, [])
    return <p>Welcome to the page</p>
    }
    import { useEffect } from 'react' import { useRouter } from 'next/router' export default function Page() { const router = useRouter() useEffect(() => { router.beforePopState(({ url, as, options }) => { // I only want to allow these two routes! if (as !== '/' && as !== '/other') { // Have SSR render bad routes as a 404. window.location.href = as return false } return true }) }, []) return <p>Welcome to the page</p> }
    import { useEffect } from 'react'
    import { useRouter } from 'next/router'
    
    export default function Page() {
      const router = useRouter()
    
      useEffect(() => {
        router.beforePopState(({ url, as, options }) => {
          // I only want to allow these two routes!
          if (as !== '/' && as !== '/other') {
            // Have SSR render bad routes as a 404.
            window.location.href = as
            return false
          }
    
          return true
        })
      }, [])
    
      return <p>Welcome to the page</p>
    }

    There is also router.back(), router.reload().

    You can listen to routing events:

    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    routeChangeStart(url, { shallow }) - Fires when a route starts to change
    routeChangeComplete(url, { shallow }) - Fires when a route changed completely
    routeChangeError(err, url, { shallow }) - Fires when there's an error when changing routes, or a route load is cancelled
    - err.cancelled - Indicates if the navigation was cancelled
    beforeHistoryChange(url, { shallow }) - Fires just before changing the browser's history
    hashChangeStart(url, { shallow }) - Fires when the hash will change but not the page
    hashChangeComplete(url, { shallow }) - Fires when the hash has changed but not the page
    routeChangeStart(url, { shallow }) - Fires when a route starts to change routeChangeComplete(url, { shallow }) - Fires when a route changed completely routeChangeError(err, url, { shallow }) - Fires when there's an error when changing routes, or a route load is cancelled - err.cancelled - Indicates if the navigation was cancelled beforeHistoryChange(url, { shallow }) - Fires just before changing the browser's history hashChangeStart(url, { shallow }) - Fires when the hash will change but not the page hashChangeComplete(url, { shallow }) - Fires when the hash has changed but not the page
    routeChangeStart(url, { shallow }) - Fires when a route starts to change
    routeChangeComplete(url, { shallow }) - Fires when a route changed completely
    routeChangeError(err, url, { shallow }) - Fires when there's an error when changing routes, or a route load is cancelled
     - err.cancelled - Indicates if the navigation was cancelled
    beforeHistoryChange(url, { shallow }) - Fires just before changing the browser's history
    hashChangeStart(url, { shallow }) - Fires when the hash will change but not the page
    hashChangeComplete(url, { shallow }) - Fires when the hash has changed but not the page
    

    About Author

    Mathias Bothe To my job profile

    I am Mathias from Heidelberg, Germany. I am a passionate IT freelancer with 15+ years experience in programming, especially in developing web based applications for companies that range from small startups to the big players out there. I create Bosycom and initiated several software projects.