useRouter

Если вы хотите получить доступ к router объекту внутри любого функционального компонента вашего приложения, вы можете использовать хук useRouter. Рассмотрим следующий пример:

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

useRouter — это React Hook, что означает, что его нельзя использовать с классами. Вы можете либо использовать withRouter, либо обернуть ваш класс в функциональный компонент.

Объект router

Ниже приведено определение объекта router, возвращаемого как useRouter, так и withRouter:

  • pathname: String — Путь текущего файла маршрута, который следует после /pages. Таким образом, basePath, locale и завершающий слеш (trailingSlash: true) не включаются.
  • query: Object — Строка запроса, преобразованная в объект, включая параметры динамического маршрута. Будет пустым объектом во время предварительного рендеринга, если страница не использует рендеринг на стороне сервера (SSR). По умолчанию {}.
  • asPath: String — Путь, отображаемый в браузере, включая параметры поиска и учитывающий конфигурацию trailingSlash. basePath и locale не включаются.
  • isFallback: boolean — Находится ли текущая страница в режиме fallback.
  • basePath: String — Активный basePath (если включен).
  • locale: String — Активная локаль (если включена).
  • locales: String[] — Все поддерживаемые локали (если включены).
  • defaultLocale: String — Текущая локаль по умолчанию (если включена).
  • domainLocales: Array<{domain, defaultLocale, locales}> — Настроенные доменные локали.
  • isReady: boolean — Обновлены ли поля роутера на стороне клиента и готовы ли к использованию. Следует использовать только внутри методов useEffect, а не для условного рендеринга на сервере. См. связанную документацию для использования с автоматически статически оптимизированными страницами.
  • isPreview: boolean — Находится ли приложение в режиме предпросмотра (preview mode).

Использование поля asPath может привести к несоответствию между клиентом и сервером, если страница рендерится с использованием рендеринга на стороне сервера или автоматической статической оптимизации. Избегайте использования asPath, пока поле isReady не станет true.

Следующие методы включены в объект router:

router.push

Обрабатывает переходы на стороне клиента. Этот метод полезен, когда next/link недостаточно.

router.push(url, as, options)
  • url: UrlObject | String — URL для перехода (см. документацию модуля URL Node.JS для свойств UrlObject).
  • as: UrlObject | String — Необязательный декоратор для пути, который будет отображаться в адресной строке браузера. До Next.js 9.5.3 использовался для динамических маршрутов.
  • options — Необязательный объект со следующими параметрами конфигурации:
    • scroll — Необязательный boolean, управляет прокруткой к верхней части страницы после навигации. По умолчанию true.
    • shallow: Обновляет путь текущей страницы без повторного выполнения getStaticProps, getServerSideProps или getInitialProps. По умолчанию false.
    • locale — Необязательная строка, указывает локаль новой страницы.

Для внешних URL не нужно использовать router.push. Для таких случаев лучше подходит window.location.

Переход на pages/about.js, который является предопределённым маршрутом:

import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  return (
    <button type="button" onClick={() => router.push('/about')}>
      Нажми меня
    </button>
  )
}

Переход на pages/post/[pid].js, который является динамическим маршрутом:

import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  return (
    <button type="button" onClick={() => router.push('/post/abc')}>
      Нажми меня
    </button>
  )
}

Перенаправление пользователя на pages/login.js, полезно для страниц, защищённых аутентификацией:

import { useEffect } from 'react'
import { useRouter } from 'next/router'

// Здесь вы бы получили и вернули пользователя
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>Перенаправление...</p>
}

Сброс состояния после навигации

При переходе на ту же страницу в Next.js состояние страницы не будет сбрасываться по умолчанию, так как React не размонтирует компонент, если родительский компонент не изменился.

pages/[slug].js
import Link from 'next/link'
import { useState } from 'react'
import { useRouter } from 'next/router'

export default function Page(props) {
  const router = useRouter()
  const [count, setCount] = useState(0)
  return (
    <div>
      <h1>Страница: {router.query.slug}</h1>
      <p>Счётчик: {count}</p>
      <button onClick={() => setCount(count + 1)}>Увеличить счётчик</button>
      <Link href="/one">one</Link> <Link href="/two">two</Link>
    </div>
  )
}

В приведённом выше примере переход между /one и /two не сбросит счётчик. Состояние useState сохраняется между рендерами, потому что родительский React-компонент Page остаётся тем же.

Если вам не нужно такое поведение, у вас есть несколько вариантов:

  • Вручную обновлять каждое состояние с помощью useEffect. В приведённом выше примере это может выглядеть так:

    useEffect(() => {
      setCount(0)
    }, [router.query.slug])
  • Использовать React key, чтобы сообщить React о необходимости перемонтировать компонент. Чтобы сделать это для всех страниц, можно использовать кастомное приложение:

    pages/_app.js
    import { useRouter } from 'next/router'
    
    export default function MyApp({ Component, pageProps }) {
      const router = useRouter()
      return <Component key={router.asPath} {...pageProps} />
    }

С объектом URL

Вы можете использовать объект URL так же, как и для next/link. Работает как для параметра url, так и для as:

import { useRouter } from 'next/router'

export default function ReadMore({ post }) {
  const router = useRouter()

  return (
    <button
      type="button"
      onClick={() => {
        router.push({
          pathname: '/post/[pid]',
          query: { pid: post.id },
        })
      }}
    >
      Нажмите здесь, чтобы прочитать больше
    </button>
  )
}

router.replace

Аналогично пропу replace в next/link, router.replace предотвращает добавление новой записи URL в стек истории.

router.replace(url, as, options)
  • API для router.replace полностью совпадает с API для router.push.

Рассмотрим следующий пример:

import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  return (
    <button type="button" onClick={() => router.replace('/home')}>
      Нажми меня
    </button>
  )
}

router.prefetch

Предварительная загрузка страниц для более быстрых переходов на стороне клиента. Этот метод полезен только для навигации без next/link, так как next/link автоматически занимается предварительной загрузкой страниц.

Это функция только для production. Next.js не предзагружает страницы в режиме разработки.

router.prefetch(url, as, options)
  • url — URL для предварительной загрузки, включая явные маршруты (например, /dashboard) и динамические маршруты (например, /product/[id]).
  • as — Необязательный декоратор для url. До Next.js 9.5.3 использовался для предзагрузки динамических маршрутов.
  • options — Необязательный объект со следующими допустимыми полями:
    • locale — позволяет указать локаль, отличную от активной. Если false, url должен включать локаль, так как активная локаль не будет использоваться.

Допустим, у вас есть страница входа, и после входа вы перенаправляете пользователя на панель управления. Для этого случая мы можем предварительно загрузить панель управления, чтобы ускорить переход, как в следующем примере:

import { useCallback, useEffect } from 'react'
import { useRouter } from 'next/router'

export default function Login() {
  const router = useRouter()
  const handleSubmit = useCallback((e) => {
    e.preventDefault()

    fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        /* Данные формы */
      }),
    }).then((res) => {
      // Быстрый переход на уже предзагруженную страницу панели управления
      if (res.ok) router.push('/dashboard')
    })
  }, [])

  useEffect(() => {
    // Предзагрузка страницы панели управления
    router.prefetch('/dashboard')
  }, [router])

  return (
    <form onSubmit={handleSubmit}>
      {/* Поля формы */}
      <button type="submit">Войти</button>
    </form>
  )
}

router.beforePopState

В некоторых случаях (например, при использовании кастомного сервера) вы можете захотеть прослушивать popstate и выполнять какие-либо действия до того, как роутер обработает событие.

router.beforePopState(cb)
  • cb — Функция, которая выполняется при входящих событиях popstate. Функция получает состояние события в виде объекта со следующими свойствами:
    • url: String — маршрут для нового состояния. Обычно это имя page.
    • as: String — URL, который будет отображаться в браузере.
    • options: Object — Дополнительные параметры, отправленные router.push.

Если cb возвращает false, роутер Next.js не будет обрабатывать popstate, и вам придётся обрабатывать его самостоятельно. См. Отключение файловой системы маршрутизации.

Вы можете использовать beforePopState для манипуляции запросом или принудительного SSR-обновления, как в следующем примере:

import { useEffect } from 'react'
import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  useEffect(() => {
    router.beforePopState(({ url, as, options }) => {
      // Я хочу разрешить только эти два маршрута!
      if (as !== '/' && as !== '/other') {
        // Заставить SSR рендерить недопустимые маршруты как 404.
        window.location.href = as
        return false
      }

      return true
    })
  }, [router])

  return <p>Добро пожаловать на страницу</p>
}

router.back

Переход назад в истории. Эквивалентно нажатию кнопки "Назад" в браузере. Выполняет window.history.back().

import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  return (
    <button type="button" onClick={() => router.back()}>
      Нажмите здесь, чтобы вернуться назад
    </button>
  )
}

router.reload

Перезагрузить текущий URL. Эквивалентно нажатию кнопки "Обновить" в браузере. Выполняет window.location.reload().

import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  return (
    <button type="button" onClick={() => router.reload()}>
      Нажмите здесь, чтобы перезагрузить
    </button>
  )
}

router.events

Вы можете прослушивать различные события, происходящие внутри роутера Next.js. Вот список поддерживаемых событий:

  • routeChangeStart(url, { shallow }) — Срабатывает, когда маршрут начинает изменяться.
  • routeChangeComplete(url, { shallow }) — Срабатывает, когда маршрут полностью изменился.
  • routeChangeError(err, url, { shallow }) — Срабатывает при ошибке изменения маршрута или отмене загрузки маршрута.
    • err.cancelled — Указывает, была ли навигация отменена.
  • beforeHistoryChange(url, { shallow }) — Срабатывает перед изменением истории браузера.
  • hashChangeStart(url, { shallow }) — Срабатывает, когда хэш изменится, но не страница.
  • hashChangeComplete(url, { shallow }) — Срабатывает, когда хэш изменился, но не страница.

Важно знать: Здесь url — это URL, отображаемый в браузере, включая basePath.

Например, чтобы прослушать событие роутера routeChangeStart, откройте или создайте pages/_app.js и подпишитесь на событие следующим образом:

import { useEffect } from 'react'
import { useRouter } from 'next/router'

export default function MyApp({ Component, pageProps }) {
  const router = useRouter()

  useEffect(() => {
    const handleRouteChange = (url, { shallow }) => {
      console.log(
        `Приложение переходит на ${url} ${
          shallow ? 'с' : 'без'
        } shallow routing`
      )
    }

    router.events.on('routeChangeStart', handleRouteChange)

    // Если компонент размонтируется, отпишитесь
    // от события с помощью метода 'off':
    return () => {
      router.events.off('routeChangeStart', handleRouteChange)
    }
  }, [router])

  return <Component {...pageProps} />
}

Мы используем Кастомное приложение (pages/_app.js) для этого примера, чтобы подписаться на событие, потому что оно не размонтируется при навигации по страницам. Однако вы можете подписаться на события роутера в любом компоненте вашего приложения.

События роутера должны регистрироваться при монтировании компонента (useEffect или componentDidMount / componentWillUnmount) или императивно при возникновении события.

Если загрузка маршрута отменяется (например, при быстром нажатии на две ссылки подряд), сработает routeChangeError. Переданный err будет содержать свойство cancelled со значением true, как в следующем примере:

import { useEffect } from 'react'
import { useRouter } from 'next/router'

export default function MyApp({ Component, pageProps }) {
  const router = useRouter()

  useEffect(() => {
    const handleRouteChangeError = (err, url) => {
      if (err.cancelled) {
        console.log(`Переход на ${url} был отменён!`)
      }
    }

    router.events.on('routeChangeError', handleRouteChangeError)

    // Если компонент размонтируется, отпишитесь
    // от события с помощью метода 'off':
    return () => {
      router.events.off('routeChangeError', handleRouteChangeError)
    }
  }, [router])

  return <Component {...pageProps} />
}

Потенциальные ошибки ESLint

Некоторые методы объекта router возвращают Promise. Если у вас включено правило ESLint no-floating-promises, рассмотрите возможность отключить его глобально или для конкретной строки.

Если ваше приложение требует этого правила, вы должны либо использовать void перед Promise, либо использовать async функцию, await для Promise, а затем void для вызова функции. Это не применимо, когда метод вызывается внутри обработчика onClick.

Затронутые методы:

  • router.push
  • router.replace
  • router.prefetch

Возможные решения

import { useEffect } from 'react'
import { useRouter } from 'next/router'

// Здесь вы бы получали и возвращали пользователя
const useUser = () => ({ user: null, loading: false })

export default function Page() {
  const { user, loading } = useUser()
  const router = useRouter()

  useEffect(() => {
    // отключить линтинг для следующей строки - Это самое чистое решение
    // eslint-disable-next-line no-floating-promises
    router.push('/login')

    // void для Promise, возвращаемого router.push
    if (!(user || loading)) {
      void router.push('/login')
    }
    // или использовать async функцию, await для Promise, затем void для вызова функции
    async function handleRouteChange() {
      if (!(user || loading)) {
        await router.push('/login')
      }
    }
    void handleRouteChange()
  }, [user, loading])

  return <p>Перенаправление...</p>
}

withRouter

Если useRouter вам не подходит, withRouter также может добавить тот же router объект к любому компоненту.

Использование

import { withRouter } from 'next/router'

function Page({ router }) {
  return <p>{router.pathname}</p>
}

export default withRouter(Page)

TypeScript

Для использования классовых компонентов с withRouter, компонент должен принимать пропс router:

import React from 'react'
import { withRouter, NextRouter } from 'next/router'

interface WithRouterProps {
  router: NextRouter
}

interface MyComponentProps extends WithRouterProps {}

class MyComponent extends React.Component<MyComponentProps> {
  render() {
    return <p>{this.props.router.pathname}</p>
  }
}

export default withRouter(MyComponent)