Комментарии 6
Подход рабочий, но у него есть цена, о которой стоит сказать прямо: ssr: false — это почти гарантированный layout shift в текущем варианте
Пока грузится JS, на странице висит заглушка (или пустота), потом компонент монтируется и рисует реальный контент. Совпадут ли они по размеру — вопрос везения. Кнопка «избранное» со счётчиком в шапке: заглушка без числа уже, чем кнопка с «12» — после гидрации шапка дёргается.
Альтернатива, при которой заглушки не нужны вообще — хранить такое состояние в куках, а не в localStorage. Куки, в отличие от localStorage, доступны серверу: читаем через cookies() из next/headers (или req.headers.cookie в Pages Router) и сразу рендерим правильный HTML. Hydration mismatch не «обходится» отключением SSR, а просто исчезает — сервер и клиент видят одни и те же данные.
Ограничение одно: куки уходят с каждым запросом и лимит ~4KB, так что это для компактных вещей — id избранного, тема, настройки. Тяжёлое можно оставить в localStorage, а в куках держать только то, от чего зависит первый рендер.
Короче: ssr: false — норм, когда состояние реально не нужно серверу и мигание некритично.
Если от него зависит видимая вёрстка — куки и SSR лучше.
Я просто делал PersistStorage для Next.js приложения, и остановился как раз таки на Cookie, потому что он доступен серверу, и просто гидрацию провожу спокойно и на сервере, и клиенте.
Единственное что ещё добавить нужно, это правильно фильтровать куки, что бы не отправлять в __HYDRATION_DATA__ или как-то так называется в доме элемент который прокидывает на клиент данные, в котором будут куки.
Я это реализовал каким образом, я для всего что должно персисится вначале добавляю к ключу название "persist_store", на выходе ключ получается такой - "persist_store_user_v1.0.0" условно, а какая-то чувствительная инфа выглядит как "store_sensitive_v1.0.0", по итогу он возьмёт только с припиской persist_store, проведёт гидрацию, и всё будет работать корректно
Да, cookies решают другой сценарий. Сервер читает значение до рендера и отдаёт HTML уже с нужным состоянием.
В проекте localStorage выбран сознательно. Избранное и история живут только в браузере, сервер их не использует. Компоненты с browser-only состоянием загружаются отдельно через ssr: false.
На текущей реализации проекта например в /goods/5 визуального layout shift при добавлении и удалении из избранного не заметил. Ошибок hydration тоже нет. Размер и поведение элементов заданы самим UI, а не случайностью. В статье есть ссылка на проект, можно проверить, что все ок.
Если persisted state влияет на первый серверный HTML, cookies подходят лучше. Если состояние нужно только после загрузки клиента, localStorage остаётся нормальным вариантом.
PersistStorage и фильтрация cookies уже отдельный паттерн. В этой статье рассматривался localStorage и client boundary в App Router.
Спасибо за комментарий.
На текущей реализации проекта например в
/goods/5визуального layout shift при добавлении и удалении из избранного не заметил.
сделайте сеть помедленнее, тогда можно заметить


Проверил проект внимательнее. Layout shift на первой загрузке действительно есть, согласен, но не при кликах после маунта. Hydration-ошибок при этом нет, ssr: false работает корректно.
Сдвиг убирается одинаковыми размерами контейнера до и после загрузки. В проекте оставлю как есть, для этого продукта считаю такую чистку избыточной
Избранное и история живут только в браузере
а когда пользователь зайдёт с другого устройства и откроет избранное, то его ждёт разочарование
Да, это учебный проект с весьма заметной кнопкой Курс, где разбирается конкретный паттерн с localStorage, ограничения которого очевидны.
Через кнопку можно попасть в линейку с проектами на Supabase и Django. Там взыскательного пользователя уже ждут аккаунт, база данных и избранное без привязки к одному браузеру

Next.js, localStorage в App Router без hydration-ошибок