Рецепты адаптивной раскладки в react приложениях с SSR

Oct 4, 2019 • 4 min read

Мистика

Недавно, при миграции части страниц нашего главного сайта на gatsby мы столкнулись со странной проблемой: одна из ссылок в главной навигации была нестилизована при первоначальной загрузке страницы. Что-то вроде:

Навигационное меню с нестилизованной ссылкой

После первого же перехода на любую другую страницу стили появлялись 🤯.

Ситуацию ещё более усугублял тот факт, что компонент, ответственный за отображение навигации, использовался и в старом приложении, использующем nextjs, но работал он там прекрасно 👻.

Расследование

Недолго думая я полез в DOM и обнаружил, что у этой ссылки css-классы относились к мобильной разметке, хотя смотрел я всё это на десктопе 🤪.

В голове крутилась мысль: react косячит при гидрации. Верить в неё конечно не хотелось: такую серьёзную проблему бы уже давно нашли, да и старом приложении ВСЁ РАБОТАЕТ.

Вспоминаю требование к гидрации:

React ожидает, что отрендеренное содержимое идентично на сервере, и на клиенте.

Бегу смотреть исходный код компонента 🕵️‍♂️:

// упрощённый NavBar.js
import React from 'react'
import Media from 'react-media'

export const NavBar = () => (
  <Media queries={{ maxWidth: 599 }}>
    {matches => (matches ? <MobileNav /> : <DesktopNav />)}
  </Media>
)

Вердикт

Видите здесь проблему?

  • В компоненте используется react-media, за счёт которого для мобильных и десктопных экранов отдаётся разная разметка
  • Мы переходим на статическую генерацию страниц (используем gatsby), т.е. SSR у нас происходит во время сборки сайта. И на этом этапе мы, конечно, не знаем на каком устройстве будет отображаться страница, а react-media при SSR по умолчанию считает все медиа запросы истинными (в нашем случае отдаст мобильную разметку).
  • Ну и получается, что пользователь с большим экраном:
    1. открывает страницу и получает с сервера html c мобильной вёрсткой (непродолжительное время видит её)
    2. видит десктопный лейаут с нестилизованной ссылкой, т.к. во время гидрации react генерирует кашу из 2-х разных лейаутов

Как быть?

Чтобы избежать таких проблем нужно:

  • Отдавать предпочтение обычным css медиа выражениям.

    Даже если с помощью CSS не получается привести одну и ту же разметку к необходимому виду на разных устройствах, можно разделить и DOM под разные разрешения/устройства и одновременно выдать все варианты, а с помощью медиа выражений отображать нужные:

    let DesktopOnly = styled.div`
      @media (max-width: 599px) {
        display: none;
      }
    `
    
    let MobileOnly = styled.div`...`
    
    export const NavBar = () => (
      <>
        <MobileOnly>
          <MobileNav />
        </MobileOnly>
    
        <DesktopOnly>
          <DesktopNav />
        </DesktopOnly>
      </>
    )
    

    В этом случае вы сможете не только избежать проблем с гидрацией, но и пользователь получит лучший опыт при работе с вашим сайтом: разметка не будет прыгать при загрузке. Т.к. браузер ещё до исполнения JavaScript отрисует сайт согласно CSS и HTML, а гидрация лишь вдохнёт в него жизнь.

  • В случае когда прибегнуть к вышеупомянутому подходу нельзя - прибегнуть к использованию react-media и аналогичных решений. Например когда:

    • может пострадать SEO (не сталкивался с подобной проблемой, но подозреваю что дублирование контента, увеличение размера страницы... может к этому привести. напишите в комментариях, если знаете кейсы)
    • может пострадать производительность (например для десктопа вы отображаете какой-то сложный элемент) ВНИМАНИЕ! Обязательно измеряйте производительность перед тем как делать подобные оптимизации. Факт того, что react'у придётся отрисовать на 50 html-блоков больше, не должен вас пугать.
    • может пострадать что-то ещё 🤷‍♂️

А как же проблема с гидрацией при использовании второго подхода?

Как я заметил в начале, эта проблема не могла остаться незамеченной и решение у неё есть - отрисовка в 2 прохода:

  • выберете раскладку которую хотите использовать по умолчанию:
    • если вы используете не статический сайт, то можете выбрать его на основе User Agent
    • для статических сайтов можете отталкиваться от статистики использования вашего сайта
  • рендерите это состояние во время серверной отрисовки и гидрации
  • запускаете дополнительную перерисовку на клиенте, если актуальное состояние отличается от того, что предполагали

Официальная документация предлагает использовать для этого флаг в стейте, и react-media такое умеет, нужно лишь указать свойство defaultMatches:

export const NavBar = () => (
  <Media
    queries={{ mobile: { maxWidth: 599 } }}
    // мы знаем что большинство наших пользователей
    // используют мобильные устройства
    defaultMatches={{ mobile: true }}
  >
    {matches => (matches.mobile ? <MobileNav /> : <DesktopNav />)}
  </Media>
)

З.Ы.

"А почему же мы не столкнулись с этой проблемой в старом приложении?" - заметит внимательный читатель. Действительно, ведь оно тоже использует SSR и не использует знание о браузере (User Agent). Если честно, я не стал тревожить легаси проект, чтобы познать его тайну 💀. Но полагаю, что дело в какой-нидь лишней перерисовке которая инициируется вышележащими компонентами.

Next reads

Рефактори как будто никто не видит

Apr 25, 2020 • 5 min read

Небольшая история о разработке codemod'ов - скриптов которые упрощают большие рефакторинги, эдакие поиск/замена на стероидах.

Первое знакомство с ReasonML

Sep 12, 2019 • 11 min read

Некоторое время назад я начал знакомиться с новым для себя языком программирования - ocaml, с его синтаксисом ReasonML если быть точным. А чтобы разобраться в нём получше я решил ещё и писать статьи. Эта будет первой (и надеюсь не последней).