Прежде чем перейти к кукам, а что вообще из себя представляет серверный компонент?
Короткий ответ: это реактовый компонент с некоторыми ограничениями, который рендерится исключительно на сервере.
В современных реалиях, где функциональные компоненты доминируют, а классовые почти полностью ушли в историю, лично мне удобно думать о компоненте как об обычной функции, возвращающей некоторую разметку (jsx). Именно такой функцией и является любой серверный компонент. Раз уж серверный компонент может быть отрендерен (читайте, "вызван") только на сервере, то внутри него может быть использована любая серверная логика (например, прямой запрос в базу данных). Однако, этот же факт запрещает использовать в нём клиентскую логику (например, с использованием состояния c помощью useState).
Помимо прочего, ввели такой термин как "клиентский компонент". Тут всё просто: это самый классический реактовый компонент. Он ничем не отличается от тех компонентов, которые вы могли писать в своих SPA с CSR. Однако, его название не должно вводить вас в заблуждение о том, что он может рендериться только на клиенте. Как и в более ранних версиях некста, такой компонент будет отрендерен на сервере, если это возможно в контексте страницы, а затем гидрирован (hydration) на клиенте тем джаваскриптом, который вы указали в теле компонента. Почему тогда "клиентский компонент"? Потому что не "серверный" :)
Ключом к успеху является грамотное чередование серверных и клиентских компонентов между собой для получения желаемого результата. Делается это по определённым правилам, узнать о которых можно из документации NextJS.
А для чего сделали такое разделение?
Если вы имели опыт с некстом до 13 версии, то наверняка в курсе о getServerSideProps. Это была незаменимая функция для динамического SSR. Как правило, она включала в себя логику для получения данных и результат её выполнения передавался в компонент страницы для дальнейшего рендеринга.
Проблема такого подхода заключалась в том, что контент всей страницы мог быть отображён только целиком за раз и только после выполнения функции getServerSideProps, которая могла включать в себя много долгих запросов. Нельзя было (по крайней мере, без костылей) отобразить одну часть страницы, параллельно догружая другую. Именно эту проблему решили в NextJS 13 за счёт стриминга и серверных компонентов.
Несколько слов про стриминг в NextJS.
Обе стороны транзакции HTTP 1.1 могут передавать свои, возможно, неограниченные данные. Преимущество заключается в том, что отправитель может отправлять данные, размер которых превышает лимит его памяти, а получатель может обрабатывать поток данных по мере поступления, вместо того, чтобы ждать прибытия всех данных. По сути, происходит экономия места и времени. Подробнее можно узнать в англоязычной статье.
В NextJS отдельные куски HTML рендерятся следующим образом:
- React преобразует серверные компоненты в специальный формат данных RSC Payload.
- Next использует RSC Payload и JavaScript-инструкции клиентских компонентов для рендеринга HTML на сервере.
Затем на клиенте:
- Сгенерированный сервером HTML используется для немедленного отображения неинтерактивной страницы.
- RSC Payload используется для согласования деревьев клиентских и серверных компонентов и обновления DOM.
- Инструкции JavaScript используются для гидрации (hydration) клиентских компонентов и делают страницу интерактивной.
Рассмотрим пример с медленным серверным компонентом:
const SlowComponent = async () => {
await new Promise((resolve) => setTimeout(resolve, 3000));
return <div>Я загрузился!</div>;
}
export default function Page() {
return (
<main>
<p>Какой-то контент</p>
<Suspense fallback={<div>Загрузка...</div>}>
<SlowComponent/>
</Suspense>
</main>
);
}
Сразу после открытия страницы имеем примерно следующий HTML (за исключением неинтересующих нас скриптов):

Через 3 секунды в рамках изначального запроса в ответ "достримятся" следующие теги:

Как нетрудно догадаться, это отрендеренный серверный компонент и скрипт для его вставки в нужное место на странице.
Так что там с куками?
При попытке выставить куку из серверного компонента, будь то layout, page или что-то ещё, вы столкнётесь со следующей ошибкой:
Почему же так происходит?
Как и любой другой сервер, некст выставляет куки с помощью http-заголовка set-cookie. Делается это с помощью вызова cookies().set()
.
Напомню, как примерно выглядит http-ответ:
HTTP/1.1 200 OK
set-cookie: foo=bar; Path=/ Vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Accept-Encoding
link: </_next/static/media/c9a5bc6a7c948fb0-s.p.woff2>; rel=preload; as="font"; crossorigin=""; type="font/woff2"
Cache-Control: no-store, must-revalidate
X-Powered-By: Next.js
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Date: Tue, 09 Jul 2024 23:22:10 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked
<!DOCTYPE html>
<html lang="en">
<head>....
Тут важно заметить, что тело ответа (в нашем случае, html) идёт в самом конце после списка заголовков. Когда мы уже начали стримить тело ответа, добавить новый заголовок становится невозможно.
Давайте вспомним пример с нашим медленным серверным компонентом. Его содержимое начинает стримиться через 3 секунды после того, как была загружена и отображена остальная часть страницы. Пытаясь выставить куку внутри него, мы по сути пытаемся добавить заголовок в http-ответ, достигший тела.
Next говорит, что куки выставлять можно только внутри Server Action или Route Handler, однако они имеют достаточно узкое применение. Если у вас, как и у меня, возникла необходимость возиться с куками авторизации, рекомендую обратить внимание на middleware и его методы по работе с куками.