Introduction
Я пообщался с сотнями разработчиков, работающих c Next.js App Router, и изучил более тысячи различных репозиториев. Вот топ 10 ошибок, которые я видел. Надеюсь, это поможет вам улучшить ваше Next.js-приложение. Давайте начнём.
Calling Route Handlers from Server Components
Первая ошибка — думать, что нужно вызывать обработчики маршрутов (route handlers) из серверного компонента React. Для начала давайте разберёмся, что такое обработчик маршрута. Обработчик маршрута позволяет вам использовать HTTP-глаголы, такие как GET, POST, PUT и DELETE, и предоставляет API для получения или изменения данных.
Для разработчиков, переходящих с маршрутизатора страниц (pages router), это может показаться похожим на API-маршруты, и здесь обычно возникает ошибка. Например, вы создаёте обработчик маршрута, который обрабатывает GET-запрос, возвращает JSON-ответ с текстом “hello world”, а затем вызываете его внутри страницы. Вы делаете её асинхронной, пишете let data = await fetch(localhost:3000/api)
, получаете JSON-данные и выводите их, например, в H1. Это работает, но фактически это ошибка по двум причинам.
Во-первых, вам приходится хардкодить полный URL, потому что вы находитесь в Node.js-контексте. Во-вторых, вам на самом деле не нужен этот лишний шаг или сетевой запрос, потому что код выполняется безопасно на сервере, как и ваш React-серверный компонент. Вместо этого вы могли бы напрямую вызвать await getUser
(или любую другую функцию или промис), не используя обработчик маршрута как промежуточное звено.
Обычно, когда я вижу такой код, я убираю лишнее. Вместо вызова через route handler я бы написал let data = await
с запросом либо к внешнему API, либо просто вызвал абстрагированную функцию напрямую. Таким образом, вы избавляетесь от ненужного слоя между вашим компонентом и данными.
Static or dynamic Route Handlers
Давайте поговорим о поведении по умолчанию для обработчиков маршрутов. Это ещё одна область, где я часто вижу путаницу. Предположим, вам действительно нужен обработчик маршрутов по какой-то причине. Обычно, когда люди используют обработчики маршрутов, это связано с необходимостью обработки вебхуков или каких-то внешних запросов в приложении. Например, для настройки чего-то вроде NextAuth. Однако часто разработчики создают обработчик маршрутов просто для тестов.
Допустим, вы сделали что-то вроде этого. Вы добавляете текущую дату: создаёте объект new Date и форматируете его в локальную строку для Нью-Йорка. Если вы откроете localhost:3000/api, вы увидите эту дату. При перезагрузке страницы дата обновляется на каждом запросе.
Здесь есть две возможности лучше понять, как работает эта модель.
- Различие между локальной и продакшн-средой. В локальной среде, даже если данные кэшируются (к этому мы вернёмся), значение создаётся заново для каждого запроса. Однако в продакшн-среде это работает иначе.
- Второе — почему данные кэшируются по умолчанию. Позвольте мне показать первое, я открываю терминал, выполняю сборку (build) и запускаю продакшн-сервер. Когда я перезагружаю страницу в продакшн-режиме, данные больше не меняются. Это демонстрирует разницу между локальной средой разработки (NextDev) и собранной версией, которая запускается на сервере в продакшне.
Почему это работает именно так? Это становится мощным инструментом, если понимать намерение. Обработчики маршрутов — это основа того, как работают страницы внутри AppRouter. Страницы по умолчанию кэшируются. Даже если вы используете асинхронный или серверный компонент и запрашиваете внешние данные, этот запрос не обязательно должен выполняться для каждого запроса в реальном времени. По сути, вместо серверного компонента это становится своего рода “компонентом данных”, поскольку вы можете извлекать данные, предварительно рендерить страницу только на сервере, и вам не потребуется дополнительный клиентский JavaScript для этого.
Короче говоря, обработчики маршрутов работают так же, как страницы. По умолчанию это статическое кэширование. Всё, что вы делаете для переключения в динамический режим, заставляет обработчик маршрута работать как традиционный API-маршрут. Например, если вы читаете что-то из request, такие как параметры запроса или заголовки, это переводит маршрут в динамический режим. Вы также можете использовать вспомогательные функции, такие как headers и cookies, чтобы читать данные из входящего запроса. Более распространённый сценарий — это POST-запросы, которые по умолчанию уже работают как динамические.
Интересная особенность в GET-запросах заключается в том, что вы можете, например, сделать let data = await fetch, получить какие-то данные и вернуть их. В локальной среде разработки вы могли бы вывести, например, информацию о своём профиле на GitHub. В старой модели маршрутизатора страниц для работы с API-маршрутом требовался сервер. Но Next.js поддерживает статический экспорт, при котором сгенерированные файлы можно разместить где угодно, например, в S3 или другом статическом хостинге.
Прелесть этого подхода в том, что даже при статическом экспорте вы можете определять произвольные маршруты, которые создают JSON, XML или текстовые файлы. Эти маршруты будут работать в модели статического экспорта. Таким образом, после сборки приложения вы можете обслуживать эти маршруты без необходимости постоянно работающего сервера.
Тем не менее, в большинстве случаев вам, вероятно, не нужно использовать обработчики маршрутов. Вместо этого вы можете вызывать необходимые функции напрямую в странице или серверном компоненте.
Route Handlers and Client Components
Итак, третье, что нужно понимать, это то, что вам нужен Route Handler, потому что вы используете клиентский компонент. На данный момент я говорил о серверных компонентах, и ваш следующий вопрос может быть: «Но я использую обработчики маршрутов, потому что у меня есть клиентские компоненты». Давайте обсудим и этот момент.
Я изменил свою страницу на клиентский компонент. Я добавил форму и кнопку, которая принимает обработчик событий для onSubmit, и затем в onSubmit я просто предотвращаю стандартное поведение и логирую «Отправлено». Так что, здесь вы могли бы сказать: «Хорошо, теперь мне нужно сделать запрос к https::/localhost/api
, получить мой обработчик маршрутов, и тогда я снова окажусь в сложной ситуации, когда мне нужно настраивать обработчики маршрутов, когда, возможно, это и не нужно».
Вот что происходит. Я нажимаю «Отправить». В консоли я вижу, что «Отправлено». Что если я скажу вам, что вам не нужно это делать таким образом, и вы можете пропустить написание всего этого дополнительного кода и просто вызвать серверное действие вместо этого? Да, серверные действия могут работать и с вашими клиентскими компонентами тоже.
Итак, что если мы удалим это и возьмем это, и скажем: «Знаете что, action будет равен send», и мы импортируем send из наших действий. Примечательно, что мы импортируем его здесь, и мы не помещаем его в строку внутри файла. Это файл с использованием клиента. Мы находимся на границе клиента, а действия выполняются на сервере.
Итак, внутри действий у нас есть use server, у нас есть эта функция, send it, и если мы вернемся сюда, нажмем «Сохранить», нажмем «Отправить». Теперь вы заметите, что позиция логирования тоже изменилась. Я не увидел, чтобы мой клиентский обработчик событий говорил, что он был отправлен в браузере. Вместо этого он просто напрямую вызвал сервер и сказал «отправить», где я бы разместил ту логику, которая изначально была в моем обработчике маршрутов.
Таким образом, это может значительно упростить вещи. Если вы хотите продвинуться дальше, есть несколько интересных возможностей, которые вы можете использовать с use optimistic и use form state, use form status, а также некоторые другие встроенные утилиты, чтобы сделать работу с формами лучше в следующем году. Так что обязательно посмотрите и это тоже.
Using Suspense with Server Components
Четвертое, это использование suspense с серверными компонентами, и я собираюсь обсудить это через призму частичной предварительной рендеринга, что является предстоящим дополнением к Next.js. В настоящее время это доступно в экспериментальном режиме, но в будущем вы начнете использовать suspense все чаще. Поэтому полезно начать думать об этом сейчас.
В этой демонстрации частичной предварительной рендеринга я перезагружаю браузер. Я вижу эти состояния загрузки. Все это определяется моими границами suspense, что очень, очень полезно. Понимание того, как работает suspense, — это большой шаг вперед для будущего Next.js. Давайте поговорим об этом.
Что у меня есть, так это серверный компонент. Он помечен как асинхронный, и я делаю запрос для получения списка блогов, а затем перечисляю их и отображаю на странице. Например, если я скажу: «Хорошо, я хочу эту функцию». Давайте скажем, что это blog posts, и мы скажем export default function page, и, возможно, я хочу добавить дополнительную информацию на этой странице. У меня есть этот раздел для моей страницы. У меня есть заголовок H1. У меня есть блог-посты. Это все выглядит фантастически.
Теперь, в мире частичной предварительной рендеринга, вы сможете предварительно рендерить все это. В данное время вы предварительно рендерите все, потому что нет резервного варианта suspense, но причина, по которой вы перемещаете этот запрос данных ближе к вашему пользовательскому интерфейсу, заключается в том, что это будет предварительно рендериться, когда вы запускаете свою сборку. Так что вы можете сказать: «Хорошо, это значит, что я хочу использовать suspense с моими блог-постами, чтобы у меня был резервный вариант и чтобы это могло быть предварительно рендерено или имело состояние загрузки».
Итак, знаете что? Давайте сделаем это. Обернем это в suspense. Мы импортируем это. У нас будет резервный вариант. Классно. Итак, у нас есть этот резервный вариант загрузки, и затем мы отображаем это здесь. Хорошо, я перезагружаю страницу, но ничего не происходит. Теперь ошибка здесь, и я не знаю, кто бы мог это сделать. Я определенно никогда не делал такую ошибку. Шучу, я определенно делал такую ошибку, в том, что вы хотите, чтобы ваша граница suspense была выше вашего компонента получения данных. Кажется очевидным задним числом, но это определенно происходит, и я понимаю, как люди могут в это попасть.
На самом деле, вам нужно что-то вроде этого. Теперь не только мой блог может быть предварительно рендерен в будущем, но и состояние резервного варианта. Вы, вероятно, не скажете «загрузка» в этом мире. Возможно, у вас будет пустое пространство, или, например, у вас будет скелет загрузки. Но да, это разница здесь.
Теперь вы, возможно, задаетесь вопросом: «Почему я не вижу это состояние?» И это потому, что по умолчанию это кешируется. Так что еще одно, что поможет нам подготовиться к будущему частичной предварительной рендеринга, это то, что я могу сказать, что хочу сделать это динамическим. И сейчас эта функция называется unstable no store. Так что я могу импортировать это здесь. Я могу перезагрузить, и теперь мы видим этот мигание. Это мигание означает, что этот элемент не должен кешироваться. Он должен выполняться динамически. Это значит, что мы видим резервный вариант suspense. Чтобы сделать это более драматичным, вы также видите «загрузка».
Надеюсь, это поможет. Мы будем говорить гораздо больше о suspense в будущем. Но, по крайней мере, сейчас, когда вы начинаете думать о структуре вашего приложения и исследовать suspense, это полезный совет для вас.
Using the incoming request
Итак, пятый совет здесь, пятая ошибка, которую я иногда вижу, это понимание того, как вы можете использовать информацию о входящем запросе в своем серверном компоненте. Например, возможно, вы хотите передать заголовки в fetch. Вы можете использовать функцию headers, чтобы прочитать эти заголовки.
Или, возможно, вы хотите прочитать куки, например, потому что хотите посмотреть на куки авторизации, которые вы сохранили, или на JWT, который вы сохранили. Возможно, вы хотите взглянуть на параметры или параметры сегмента маршрута в вашем URL. Эти параметры, а также параметры поиска URL, на самом деле являются свойствами серверного компонента. У вас есть params и search params.
Таким образом, если бы я хотел, например, сказать, давайте здесь вместо этого сделаем заголовок h1 и будем читать search params. Давайте скажем, hello. Отлично. Итак, внутри этого я собираюсь создать новый параметр поиска hello и сказать world. Теперь я могу прочитать эту входящую информацию из запроса, или я могу прочитать параметры. Если у меня есть динамические сегменты маршрута в моем URL, я могу прочитать их тоже.
Таким образом, есть несколько различных способов, с помощью которых вы можете читать информацию из этого входящего запроса, используя некоторые из этих встроенных функций в Next.js.
Using Context providers with App Router & Using Server and Client Components together
Итак, шестая и седьмая самые распространенные ошибки связаны с тем, как использовать контекстные провайдеры и где их размещать в вашем приложении, иногда они размещаются неправильно. Вторая ошибка — это переплетение или чередование серверных и клиентских компонентов.
Например, если у меня есть макет маршрута здесь слева, вы можете подумать: «Хорошо, у меня есть некоторые компоненты, у меня есть зависимости, которые требуют контекста React. Где мне разместить этот провайдер в моем приложении? Нужно ли мне помещать его вокруг самого нижнего компонента в дереве? Нужно ли мне иметь его в верхней части дерева? Значит ли это, что все ниже будет клиентским компонентом?»
Эта схема объясняется в документации. Я хочу немного визуально показать это здесь. Давайте представим, что у меня есть провайдер темы здесь, и он использует контекст React. Теперь он может принимать некоторые дочерние элементы, и это клиентский компонент, но идеальное место для его размещения — это на самом деле в макете маршрута.
Таким образом, то, что мы хотим сделать, как мы показываем здесь, — это взять этот провайдер темы и обернуть им дочерние элементы нашего макета маршрута, то есть обернуть им дочерние элементы нашего приложения. Это все еще серверный компонент, и это по-прежнему позволяет дочерним элементам, таким как страница, быть серверными компонентами. Таким образом, дочерний элемент серверного компонента может быть клиентским компонентом. Это работает так, как мы ожидаем.
Adding “use client” unnecessarily
Это на самом деле переходит к следующей самой распространенной ошибке, которую я вижу, а именно, когда вы применяете use client, распространяется ли это на все? Распространяется ли это на его дочерние элементы, и как работает эта связь? И я скажу, что это все новое, поэтому, хотя я подаю это как ошибки, я думаю, что это все возможности для обучения для нас, так что не переживайте слишком сильно. Знаете, я допускал все эти ошибки в этом видео.
Таким образом, этот верхний уровень макета — это серверный компонент, но дочерним элементом является этот дочерний пропс, который идет к странице. Итак, страница является дочерним элементом макета. Теперь дочерним элементом этой страницы является компонент кнопки. Давайте взглянем на этот компонент кнопки. Этот компонент кнопки — это простой клиентский компонент, у которого есть счетчик, то есть у него есть состояние. Вы нажимаете на него, состояние увеличивается, и мы видим это новое состояние, отраженное в тексте кнопки.
Таким образом, это уже показывает, как вы можете сочетать серверные и клиентские компоненты, но вопрос тогда становится: что насчет дочерних элементов этого компонента? Например, давайте скажем, что вместо этого я просто сделаю фрагмент, и затем у меня будет это, но также у меня будет другая кнопка, и эта другая кнопка также является клиентским компонентом. Итак, у меня есть эта кнопка, у меня есть другая кнопка и еще одна кнопка здесь. Нужно ли мне явно указывать use client вверху?
Ну, ответ давайте выясним. Ответ — нет, и причина, по которой это не нужно, в том, что вы уже находитесь в границе клиента здесь. Таким образом, дочерние элементы этого компонента будут клиентскими компонентами, если, например, я возьму какие-то дочерние элементы, и давайте скажем, что я хочу иметь дочерние элементы здесь, вы все равно можете вплетать и серверные компоненты тоже.
Так что, возможно, здесь, например, я знаю, что это немного надуманное пример, но, тем не менее, это работает. Это клиентский компонент, это клиентский компонент, это серверный компонент, и они могут все работать вместе. Большая возможность здесь, и почему, да, это ошибка, но это также возможность, заключается в том, что нам нужны инструменты разработчика, которые помогут вам визуализировать эти вещи. Мы определенно работаем над улучшением этого опыта, чтобы вы могли понять, как ваши серверные и клиентские компоненты работают вместе.
Not revalidating data after mutations
Хорошо, еще две ошибки, и вторая из последних. Ошибка, которую я вижу чаще всего, — это просто забывание повторной валидации данных после изменения. В репозитории Next.js у нас есть пример форм, который я рекомендую вам посмотреть. Он показывает серверный компонент, который получает список задач. Он отображает список задач в виде формы для добавления новой задачи, и внутри этой формы используются многие последние функции, такие как серверные действия.
У меня есть действие для отправки формы для добавления новой задачи, и что я вижу, так это то, что многие люди начинают использовать серверные действия. Возможно, они переходят с более старой модели, где пытались сопоставить это один к одному с обработчиками маршрутов, и переходят на это, нажимают кнопку и отправляют, но не видят обновления списка задач и пытаются выяснить, что именно происходит.
Что они, вероятно, упускают, так это то, что внутри их действия. В этом случае у меня есть SQL-запрос, который вставляет новое значение в таблицу задач. Что они, вероятно, пропускают, так это некоторую повторную валидацию, либо повторную валидацию пути, либо добавление тега кеша к запросу или к этому фрагменту данных, а затем повторная валидация этого конкретного тега.
Таким образом, довольно часто я вижу это. Я знаю, что кеширование сейчас все еще очень новое, и многие из этих паттернов тоже новые. Мы также работаем над тем, чтобы сделать кеширование немного проще, но это одна из распространенных ошибок, которую я вижу.
Redirects inside of try/catch blocks
Хорошо, и пока мы здесь, мы можем поговорить о последней ошибке, которая заключается в том, что вы выбрасываете перенаправление внутри блока try-catch. Я понимаю, как это происходит. Я определенно сам допускал эту ошибку. Вы находитесь здесь, вы повторно валидировали свой путь и говорите: «Хорошо, отлично, теперь я хочу перенаправить обратно на индексный маршрут», например.
На самом деле, внутренний механизм работы этого перенаправления заключается в том, что он фактически выбрасывает специфическую для XGS ошибку. Поэтому вам нужно будет разместить это в заключении, либо после блока try-catch, либо в блоке finally. Я думаю, что чаще всего я вижу это либо в серверном действии, либо если вы пытаетесь использовать это из клиентского компонента.
Недавно я обновил документацию, чтобы показать пример. Когда вы используете серверный компонент, например, вместо того чтобы пытаться сделать перенаправление внутри обработчика событий, вам нужно, чтобы это было внутри вашего серверного действия. Если вы хотите сделать перенаправление на стороне клиента в обработчике событий, вам нужно использовать хук use router, который действительно имеет возможность программно маршрутизировать и программно перенаправлять.