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.

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:

    // 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:

    // 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.

    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.

    import Link from 'next/link'
    
    export default function FirstPost() {
      return (
        <>
          <Link href="/">Home</Link>
        </>
      )
    }

    Linking to dynamic routes

    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:

    <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:

    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:

    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:

    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:

    // 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

    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

    import { useRouter } from 'next/router'
    
    export default function Page() {
      const router = useRouter()
    
      return <button onClick={() => router.replace('/home')}>Click me</button>
    }

    beforePopState

    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:

    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, born 39 years ago in Heidelberg, Germany. Today I am living in Munich and Stockholm. I am a passionate IT freelancer with more than 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 am founder of bosy.com, creator of the security service platform BosyProtect© and initiator of several other software projects.