Comments 32
Что ж, период активного чтения статьи только завершился, как ко мне пришла идея как реализовать то, чего так не хватало.
Так что, теперь пакет содержит Серверные Контексты!
https://github.com/vordgi/next-impl-getters?tab=readme-ov-file#server-contexts-beta
Быть может, дойдут руки до ещё одной статьи по next.js, давно планирую собрать статью как оно там устроено...
По багам не вписалось в статью, поэтому оставлю это в комментариях.
В 11 версии было решено завести i18n решение от next.js в коммерческий проект. Пара недель работы и… Странный баг, что вариант /en/en/page-name отображался как /page-name и разные артефакты от этого.
Как так вышло? Next.js проверял путь на наличие региона в начале и удалял его. Но эта логика повторялась примерно в десятке мест. Его удалось исправить в ядре, но оказалось, что для дефолтной локали всё ещё хуже и это не единственный артефакт. Как итог - откат.
Это произошло при тысячах тестов. Увы, но часто баги возникают именно на грани связок подобных параметров (таких как basePath, assetsPrefix, i18n).
Предположу, что этот функционал стал слишком провальным, так как next.js в новой версии даже не упоминает его и предлагает это делать через динамический сегмент (часть пути, в качестве которой могут быть разные варианты, в данном случае, например /[lang]/page-name) и middleware (как по итогу и было сделано в проекте).
Добрый день. Пользуюсь next.js не так давно. До этого писал только на angular. Согласен в целом со всем, фрэймворк выглядит интересным и многообещающим но многие вещи просто вводят в тупик. К примеру описанная вами проблема с пробрасыванием раутов или любых других данных в серверных компонентах. Я решил ее кастомной имплементацией кэша из next.js. В корне page.tsx добавляю данные в компонентах ниже - считываю. Возможно ваша библиотека делает тоже самое, не смотрел в деталях.
Но вопрос в другом - решили ли вы ( и решали ли) проблему с невозможностью переиспользования части функциональности между клиентскими и серверными компонентами? Пример банальный - узнать мобильное устройство используется или нет. На сервере можно использовать headers реквеста, в клиентском компоненте - media query. Но переиспользовать это решение нельзя, что ведет к другой проблеме - в клиентских компонентах нужно создавать дополнительный инпут пропс чтобы прокинуть начальное значение isMobile. Ведь иначе серверный рендеринг (изначальный) для клиентского компонента отрисует неправильный html, который потом будет прыгать когда подтянется значение из хука и media query. И это ведет к props drilling уже в клиентских компонентах.
Добрый!
Возможно ваша библиотека делает тоже самое
Не, путь и параметры самое базовое API и просить оборачивать все страницы с контекст для такого - ужасный DX (да и не везде вообще есть такая возможность). Пакет использует внутреннее API next.js.
На сервере можно использовать headers реквеста
headers включает режим серверного рендеринга, поэтому мы от него отказались и не используем в компонентах.
который потом будет прыгать когда подтянется значение из хука и media query
А почему нельзя сразу обойтись стилями и всё? Зачем это делать логикой?
Если компонент завязан на клиента и стилями никак не решить - делаем его только клиентским:
const ClientOnly = dynamic(() => import('../components/ClientOnly'), { ssr: false })
-------
Пара условий, которые могут помочь в разных похожих ситуациях.
И так проверяет - клиентский компонент или серверный next.js использует внутри проверку:
if (React.useState) {...}
Для проверки где сейчас рендерится клиентский компонент:
if (typeof window === 'undefined') {...} // на сервере
А чем Вам не нравится режим серверного рендеринга?
Стили не всегда могут помочь. Самый базовй пример, у меня есть компонент кнопки. Пропсами в него передаются тип и размер. Т.е. для того, чтобы поменять размер кнопки с md на sm мне нужно изменить инпут параметр. Соответственно при перезагрузке страницы, когда серварная часть возвращает уже готовый html мне нужно знать мобильная версия или нет. И если компонент помечен как client, то mediaQuery отработает только в браузере, и кнопка поменяет размер.
Такая же проблема с изменением количества столбцов в masonry layout. У нас есть кнопка тригер, которая меняет кол-во столбцов, плюс разные возможные значения для мобильной и десктопных версий. Т.е. контролируется это через код, а не стили. Вот и получается, что в клинтском компоненте (при начальном рендеринге) я могу понять мобилка это или нет только через прокидование пропсов с серверных компонентов, в которых смотрю в headers.
Да, можно в теории передавать стили внутрь компонента, но мне не кажется это хорошей идеей.
Дополняю:
Вспомнил про еще один кейс - проверка авторизации и изменения view основываясь на типе пользователя (не зарегистрированный, зарегистрированный, премиум). Сейчас для этого есть два разных функционала - один для серверных компонентов, который смотрит в nextjs cookies. И второй - контекст для клиентских компонентов, который будет работать во время spa поведения. И опять же сталкиваемся с проблемой пререндеринга на стороне сервера. (Как объяснить клиентскому компоненту какой вариант использовать для пререндеринга, если не прокидывать пропсы на кучу уровней внутрь?) Отключить его полность (ssr: false) - не вариант, т.к. есть серьезные требования к seo и crawling от поисковиков.
А чем Вам не нравится режим серверного рендеринга?
Маркетинговый сервис, где в общем-то учитываем потери даже на доли секунды. Ну и следствие маркетингового - нет критической завязки на конкретного клиента, мобильная и десктопная версия делаются одинаковые (чтоб лишний раз роботов не пугать).
Но я бы при такой ситуации сделал что-то вроде:
'use client';
// ...
const WithUserDeviceContext = ({ children, defaultDevice }) => {
const [userDevice, setUserDevice] = useState(defaultDevice);
return (
<UserDeviceContext.Provider value={userDevice}>
<SetUserDeviceContext.Provider value={setUserDevice}>
{children}
</SetUserDeviceContext.Provider>
</UserDeviceContext.Provider>
)
}
const WithUserDevice = ({ children }) => {
const headers = headers();
const defaultDevice = ...;
return (
<WithUserDeviceContext defaultDevice={defaultDevice}>
{children}
</WithUserDeviceContext>
)
}
const ParentServerComponent = () => {
<WithUserDevice>
<MasonryLayout />
</WithUserDevice>
}
'use client';
const MasonryLayout = () => {
const userDevice = useContext(UserDeviceContext);
const setUserDevice = useContext(SetUserDeviceContext);
useEffect(() => {
// ...
setUserDevice();
}, [])
return (
// ...
)
}
Действительно работает!
Спасибо большое, видимо, отсутствие опыта с react сказывается. Думал про всякие динамически импорты и тому подобное, но просто сделать обертку не додумался. Единственное, что использовал контекст в root layout чтобы все используемые в проекте компоненты могли использовать, т.к. это глобальная фича.
Подсажите еще, пожалуйста, зачем разделять UserDeviceContext и SetUserDeviceContext ? Для того, чтобы уменшить вляние ререндеринга при измении любой из пропертей контекста и вместо этого пользоваться мемозацией примитивов?
Да. Обычно так делают чтобы разделить логику - бывают компоненты только меняют состояние и им не важно какое состояние сейчас, а другие наоборот только читают.
Первым вообще не нужно ререндериться, поэтому им не нужно зависеть от UserDeviceContext.
А вторым не нужно знать логику смены состояния - небольшой бонус в виде деления ответственности.
Скажите, а что лучше - ангулар или некст? Просто я знаю неплохо ангулар тыкал некст - и мне нг показался более завершенным и строгим. В 17й версии вон юниверсал вообще впихнули в ядро
Интересно, умеет ли app router работать с полноценными синонимами URL, когда нет возможности только лишь по виду URL определить тип страницы? Например, есть страница товара и страница статьи - это два разных компонента. На сайте 100500 статей и товаров с урлами типа /abcd /bcda и т.д. и нужно их рендерить в разных компонентах. Предлагает ли app router что-нибудь для решения этой задачи? Три года назад мне пришлось писать для этого полностью кастомный роутер плюс кастомный компонент ссылки, чтобы обрабатывать ещё и client side роутинг.
Ну так как это по умолчанию серверные компоненты (а страницы всегда стоит держать серверными, иначе нельзя будет настроить для них мета-заголовки), то можно сделать просто общий роут и в нём возвращать либо один компонент, либо другой (код на коленке):
const ProductOrPostPage = async ({ params }) => {
if (await isProduct(params.slug)) {
return <ProductPage slug={params.slug} /> // и внутри получать продукт, также асинхронно
}
return <PostPage slug={params.slug} /> // и внутри получать статью, также асинхронно
}
const ProductOrPostPage = async ({ params }) => {
const product = await getProduct(params.slug);
if (product) {
return <ProductPage title={product.title} />
}
const post = await getPost(params.slug);
if (!post) return notFound();
return <PostPage title={post.title} />
}
Ну а когда сайт полностью статический - должно хватить generateStaticParams (но думаю раз раньше не хватило getStaticPaths - скорее всего и это не поможет)
-----
А как проверяли какой вариант возвращать в кастомном роутере?
А как проверяли какой вариант возвращать в кастомном роутере?
Делал запрос к бэкенду.
то можно сделать просто общий роут и в нём возвращать либо один компонент, либо другой
Изначально так и планировалось, но потом почему-то возникла необходимость ещё до начала рендеринга знать тип страницу. Не помню, почему, но знаю, что через год, как я ушёл, кто-то пытался всё это выпилить и упростить, а через две им недели пришлось вернуть всё как было.
а страницы всегда стоит держать серверными, иначе нельзя будет настроить для них мета-заголовки
А для чего тогда компонент Head придумали, он прекрасно с этим справляется? Я пользовался next.js с 6 версии. И тогда мне очень понравилось то, что это было изоморфное решение, где один и тот же код отрабатывал на сервере при запросе первой страницы, а затем он уже работал в браузере. Всё это делалось с помощью getInitialProps. И для меня было огромным разочарованием, когда этот метод депрекейтнули. Ещё тогда я заметил, если у нас есть отдельный бэкенд, скажем на php, который просто отдаёт данные в json, бутылочным горлышком всей системы был сервер Next.js. Клиентские переходы как правило происходили заметно быстрее, чем запрос с сервера отрендеренной страницы. Очевидно, и сами разрабы некста это признали, когда придумали статическую генерацию. По факту это значит только одно: "Ребята, мы не можем сделать приемлемой скорость рендеринга в next.js, поэтому вот держите генератор статики". Но статика не подходит для проекта, где 100.000 страниц, и в день добавляется или обновляется тысяча из них. Но бог с ней, со статикой, они и динамику испортить умудрились. Сейчас вместо метода getInitialProps рекомендуют getServerSideProps. И теперь вместо того, чтобы бросить с клиента запрос к API бэкенда и получить мгновенно закэшированный ответ, запросы идут всё на тот же сервер next.js, который и так нагружен рендерингом, так ещё эти запросы некэшируемые. ИМХО, последние годы развития next.js - это скорее деградация, нежели развитие.
А больше нет компонента Head ?
Вместо него generateMetadata - https://nextjs.org/docs/app/api-reference/functions/generate-metadata (работает похожим образом как getInitialProps, но возвращает не пропсы, а объект метаданных, которые некст сам соберёт - тоже сомнительное на самом деле)
На самом деле по скорости, особенно с серверными компонентами - показывает себя хорошо. Ещё в новой версии ISR работает достаточно удобно и задача обновить 1000 страниц звучит как вполне простая - вместо ревалидации массива из 1000 страниц, вызывая метод для каждой, можно сказать - пересобери все страницы, которые использовали таг posts-javascript и он это в фоне сделает (https://nextjs.org/docs/app/api-reference/functions/revalidateTag).
Лет пять назад мне нужно было начать писать проект на Реакт, но с ССР... Наслушавшись и начитавшись полез я, значит, в новомодные фреймворк, остановился на Некст и пошел обратно...
В итоге проект написали на webpack и express. И чуть позже добавили сплит.
В прошлом году возник вопрос полного редизайна и возможность потихоньку переползти на что-то другое. Например, на next, все он мне покоя не давал.
Лично дл себя я решил, что нет милее собственной сборки на webpack. А всякие next.js и CRA для тех кто думает иначе ?
Подскажите пожалуйста статью, где более подробно описано кеширование в next, как развернуть правильно next в облаке, какие ресурсы выделяются для Кеша. Спасибо
Подробно кеширование описали сами next.js - https://nextjs.org/docs/app/building-your-application/caching;
По разворачивания тоже у них есть инструкция с примерами - https://nextjs.org/docs/app/building-your-application/deploying#self-hosting;
По ресурсам:
in memory - 50мБ (можно поменять в конфиге)
В случае дефолтного обработчика кеша не в Vercel - всё сохраняется в файловой системе (насколько знаю без лимитов);
В случае дефолтного в Vercel - общий лимит не знаю (скорее всего его нет), но есть лимит в 2мБ на запрос. Весьма вероятно в будущем кеширование в Vercel сделают платным;
В случае кастомного - как настроете
Был фанатом Некста много лет, но с появлением HTMX у меня теперь большие сомнения в целесообразности использования React для рендеринга страниц целиком. Свой будущий проект я бы предпочел делать на Astro + HTMX. Хотя HTMX подойдёт для любого бэка.
Что ж, период активного чтения статьи только завершился, как ко мне пришла идея как реализовать то, чего так не хватало.
Так что, теперь пакет содержит Серверные Контексты!
https://github.com/vordgi/next-impl-getters?tab=readme-ov-file#server-contexts-beta
Быть может, дойдут руки до ещё одной статьи по next.js, давно планирую собрать статью как оно там устроено...
Выглядит мега полезно, возникает вопрос, почему не было этого из коробки и почему они не принимают PR.
Еще вопрос, как это срастить с клиентским контекстом. Было бы здорово собрать все на серваке с серверным контекстом, а потом, на клиенте, поменять его. Например:
Проверка авторизации и рендер в зависимости от наличия авторизации и/или прав
Позиция команды Next.js.
Что-то вроде "Вся динамика должна быть на клиенте. Клиентские компоненты не хуже серверных и с ними не будет потерь в оптимизации". Не знаю, зачем с такими взглядами вообще их завезли.
Ещё есть проблема с Layout-ом, которую я в PR не решил - он не ререндерится и если использовать этот геттер внутри лайаута - он не обновится при переходе между страниц. Тут нужно просто блочить всю динамику внутри лайаута, но пока next.js так не умеет.
В такой ситуации думаю лучше создать провайдер с клиентским и серверным контекстом и передавать в него пользователя.
const UserProvider = ({ user, children }) => (
<ServerUserContext user={user}>
<ClientUserContext user={user}> // внутри useState/redux/что угодно
{children}
</ClientUserContext>
</ServerUserContext>
)
Тоже непонятен этот подход, какой-то странный hydrate получается, когда при серверном рендеринге куска просто нет =)
Вместо Layout можно использовать Template, он как раз обновляется при переходах.
Вот такой провайдер не получится сделать, при компиляции клиентской части не найдет async_hooks, да и useContext должен быть разный. То есть в идеале концепция одна Context provider + useContext, на серверной стороне это будет через async_hooks, а на клиентской через react.
Попробовал геттер в темплейте, компонент обновляется, но вот pathname остается тот же... то есть компонент просто ремаунтится =(
Там вероятно есть клиентское кеширование темплейта. Спасибо за случай, обдумаю.
А работать такое [как я написал выше] должно, попробую на днях добавить пример.
В общем, мне кажется, я понял логику команды Nextjs: вам не нужен серверный контекст, потому что мы сделали кеширование для fetch. Пример: нужно рендерить что-то в зависимости от авторизации? Создайте api/user и делайте запрос getUser()
во всех своих компонентах, где нужно получить эти данные. Потому что они будут из кеша.
Хорошо это или плохо я пока не понял...
С одной стороны, в рамках рект-логики это непривычно, с другой стороны, в рамках чисто серверных приложений, такой подход живет, где у нас вместо fetch запроса запрос к бд, например.
Наконец дошли руки доделать пример с таким провайдером
https://github.com/vordgi/next-impl-getters/tree/main/examples/isomorphic-context
Нашел еще крутой use case: i18n. Для переводов необходим контекст, иначе придется прокидывать все в пропсы. То есть без единого контекста в клиентских файлах, которые отрендерит сервер, не будет перевода, а будет заглушка типо header.chat.title
и только позже подхватится на клиенте, но прилетит content did not match
. Я пока так и не придумал как это обойти.
Так, тут я затупил, обычные контексты работают же в SSR, если это клиентский компонент
Ну с переводами сложно, да. Для них я по итогу сделал своё решение и об этом следующая статья)
Больше библиотек богу библиотек или как я переосмыслил i18n [next.js v14]
В общем, еще одна проблема, и, вероятно, причина, почему серверный контекст в текущей реализации next не имеет смысла: next рендерит дерево снизу вверх, то есть сначала вложенные страницы, затем вложенные лейауты, затем лейаут выше и т.д. Подробнее: https://github.com/vercel/next.js/discussions/53026
Layout просто совсем непрактичная штука и подходит для очень ограниченного списка проектов.
Мои взгляды, что на layout нужно забивать и, если там используются контексты/получение пути/разных данных страницы - кидать ошибку. Применение контекстов можно посмотреть в библиотеке next-translation, где я рекомендую инициализировать провайдер на каждой странице.
А вообще Next.js собирает страницу сверху вниз, он проходит сегмент за сегментом собирая абстракции на текущем уровне, но слои собираются отдельно от страниц. Там страшная логика, но основное лежит здесь: https://github.com/vercel/next.js/blob/79b7cb5f075c26698bc2cb8a569cda8a6e3b49bd/packages/next/src/server/app-render/create-component-tree.tsx#L433

шикарная статья! согласен почти со всем! спасибо большое что смогли так точно описать поворот не туда!
Next.js App Router. Опыт использования. Путь в будущее или поворот не туда