Страницы и макеты

Роутер Pages Router основан на файловой системе и работает по принципу страниц.

Когда файл добавляется в директорию pages, он автоматически становится доступным как маршрут.

В Next.js страница — это React-компонент, экспортируемый из файла .js, .jsx, .ts или .tsx в директории pages. Каждая страница связана с маршрутом в соответствии с именем файла.

Пример: Если создать файл pages/about.js, экспортирующий React-компонент, как показано ниже, он будет доступен по адресу /about.

export default function About() {
  return <div>About</div>
}

Корневые маршруты (index)

Роутер автоматически связывает файлы с именем index с корнем директории.

  • pages/index.js/
  • pages/blog/index.js/blog

Вложенные маршруты

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

  • pages/blog/first-post.js/blog/first-post
  • pages/dashboard/settings/username.js/dashboard/settings/username

Страницы с динамическими маршрутами

Next.js поддерживает страницы с динамическими маршрутами. Например, если создать файл pages/posts/[id].js, он будет доступен по адресам posts/1, posts/2 и т.д.

Подробнее о динамической маршрутизации можно узнать в документации по Dynamic Routing.

Шаблон макета

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

components/layout.js
import Navbar from './navbar'
import Footer from './footer'

export default function Layout({ children }) {
  return (
    <>
      <Navbar />
      <main>{children}</main>
      <Footer />
    </>
  )
}

Примеры

Общий макет с Custom App

Если для всего приложения нужен только один макет, можно создать Custom App и обернуть приложение в макет. Поскольку компонент <Layout /> повторно используется при переходе между страницами, его состояние (например, значения полей ввода) сохраняется.

pages/_app.js
import Layout from '../components/layout'

export default function MyApp({ Component, pageProps }) {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  )
}

Отдельные макеты для страниц

Если требуется несколько макетов, можно добавить свойство getLayout на странице, позволяющее возвращать React-компонент для макета. Это позволяет определять макет для каждой страницы индивидуально. Поскольку мы возвращаем функцию, можно создавать сложные вложенные макеты.

pages/index.js

import Layout from '../components/layout'
import NestedLayout from '../components/nested-layout'

export default function Page() {
  return (
    /** Ваш контент */
  )
}

Page.getLayout = function getLayout(page) {
  return (
    <Layout>
      <NestedLayout>{page}</NestedLayout>
    </Layout>
  )
}
pages/_app.js
export default function MyApp({ Component, pageProps }) {
  // Используем макет, определённый на уровне страницы, если он доступен
  const getLayout = Component.getLayout || ((page) => page)

  return getLayout(<Component {...pageProps} />)
}

При переходе между страницами мы хотим сохранять состояние страницы (значения полей ввода, позицию прокрутки и т.д.) для работы в режиме SPA (Single-Page Application).

Этот шаблон макета позволяет сохранять состояние, поскольку дерево React-компонентов поддерживается между переходами. React может определять, какие элементы изменились, чтобы сохранить состояние.

Полезно знать: Этот процесс называется согласованием (reconciliation), который позволяет React определять изменённые элементы.

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

При работе с TypeScript сначала нужно создать новый тип для страниц, включающий функцию getLayout. Затем необходимо создать тип для AppProps, переопределяющий свойство Component с использованием ранее созданного типа.

import type { ReactElement } from 'react'
import Layout from '../components/layout'
import NestedLayout from '../components/nested-layout'
import type { NextPageWithLayout } from './_app'

const Page: NextPageWithLayout = () => {
  return <p>hello world</p>
}

Page.getLayout = function getLayout(page: ReactElement) {
  return (
    <Layout>
      <NestedLayout>{page}</NestedLayout>
    </Layout>
  )
}

export default Page
import Layout from '../components/layout'
import NestedLayout from '../components/nested-layout'

const Page = () => {
  return <p>hello world</p>
}

Page.getLayout = function getLayout(page) {
  return (
    <Layout>
      <NestedLayout>{page}</NestedLayout>
    </Layout>
  )
}

export default Page
import type { ReactElement, ReactNode } from 'react'
import type { NextPage } from 'next'
import type { AppProps } from 'next/app'

export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
  getLayout?: (page: ReactElement) => ReactNode
}

type AppPropsWithLayout = AppProps & {
  Component: NextPageWithLayout
}

export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
  // Используем макет, определённый на уровне страницы, если он доступен
  const getLayout = Component.getLayout ?? ((page) => page)

  return getLayout(<Component {...pageProps} />)
}
export default function MyApp({ Component, pageProps }) {
  // Используем макет, определённый на уровне страницы, если он доступен
  const getLayout = Component.getLayout ?? ((page) => page)

  return getLayout(<Component {...pageProps} />)
}

Получение данных

В макете можно получать данные на стороне клиента с помощью useEffect или библиотек, таких как SWR. Поскольку этот файл не является страницей, в нём нельзя использовать getStaticProps или getServerSideProps.

components/layout.js
import useSWR from 'swr'
import Navbar from './navbar'
import Footer from './footer'

export default function Layout({ children }) {
  const { data, error } = useSWR('/api/navigation', fetcher)

  if (error) return <div>Failed to load</div>
  if (!data) return <div>Loading...</div>

  return (
    <>
      <Navbar links={data.links} />
      <main>{children}</main>
      <Footer />
    </>
  )
}