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 — Находится ли приложение в режиме предпросмотра.

Использование поля asPath может привести к несоответствию между клиентом и сервером, если страница рендерится с использованием SSR или автоматической статической оптимизации. Избегайте использования 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 Router. Вот список поддерживаемых событий:

  • 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-маршрутизации`
      )
    }

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

Экспорт next/compat/router

Это тот же самый хук useRouter, но его можно использовать как в директории app, так и в pages.

Он отличается от next/router тем, что не выбрасывает ошибку, когда роутер страниц не подключен, а вместо этого имеет тип возвращаемого значения NextRouter | null. Это позволяет разработчикам адаптировать компоненты для работы как в app, так и в pages во время перехода на роутер app.

Компонент, который раньше выглядел так:

import { useRouter } from 'next/router'
const MyComponent = () => {
  const { isReady, query } = useRouter()
  // ...
}

Выдаст ошибку при переходе на next/compat/router, так как null нельзя деструктуризировать. Вместо этого разработчики могут воспользоваться новыми хуками:

import { useEffect } from 'react'
import { useRouter } from 'next/compat/router'
import { useSearchParams } from 'next/navigation'
const MyComponent = () => {
  const router = useRouter() // может быть null или экземпляром NextRouter
  const searchParams = useSearchParams()
  useEffect(() => {
    if (router && !router.isReady) {
      return
    }
    // В `app/` searchParams будут доступны сразу со значениями,
    // в `pages/` они станут доступны после готовности роутера.
    const search = searchParams.get('search')
    // ...
  }, [router, searchParams])
  // ...
}

Теперь этот компонент будет работать как в директории pages, так и в app. Когда компонент больше не используется в pages, можно удалить ссылки на совместимый роутер:

import { useSearchParams } from 'next/navigation'
const MyComponent = () => {
  const searchParams = useSearchParams()
  // Так как компонент используется только в `app/`, совместимый роутер можно удалить.
  const search = searchParams.get('search')
  // ...
}

Использование useRouter вне контекста Next.js в pages

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

import { renderToString } from 'react-dom/server'
import { useRouter } from 'next/compat/router'
const MyComponent = () => {
  const router = useRouter() // может быть null или экземпляром NextRouter
  // ...
}
export async function getServerSideProps() {
  const renderedComponent = renderToString(<MyComponent />)
  return {
    props: {
      renderedComponent,
    },
  }
}

Возможные ошибки 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)