React-хук для роботи з matchMedia

Готовий кастомний хук. Достатньо скопіювати — і матимете зручну можливість отримувати дані від matchMedia, задавати кастомні медіа-запити, легко їх змінювати та використовувати все це у React.

У попередній статті про різноманітні підходи до роботи над адаптивністю веб-сайтів я згадувала matchMedia — JavaScript API, який дозволяє отримувати відповіді true/false на будь-який ваш медіа-запит та керувати кодом в залежності від розміру екрану. Це напрочуд крутий інструмент в разі якщо у вашому проекті багато елементів, що мають реагувати на зміни екрану у корстувача. Проте його не можна використовувати за просто так у React. Як спеціаістка, що найбільше працює саме з рекатом, я написала кастомний хук, яким ділюся з вами.

Повний код хука на CodePen

А тепер по порядку

В статті я розказую про:

  1. npm-пакет для роботи з matchMedia, який вже існує

  2. плюси свого хука

  3. що відбувається під капотом


На npm можна знайти пакет react-use-match-media для роботи з matchMedia в React і він більш ніж корисний, якщо у вас в проекті необхідність звертатись до медіа-запитів саме через js сягає 1-3 разів на весь код. Тоді не треба винаходити велосипеда, просто беріть цей пакет. Приблизно так його використовують:

const Example = (props) => {
  const isWideViewport = useMatchMedia('(min-width: 600px)');

  return <div>{isWideViewport ? 'Wide' : 'Narrow'}</div>;
};

Незручніть полягає в тому, що хук стає важким у застосуванні в разі, коли ваш проект потребує багато різних медіа-запитів в різних місцях коду. По-перше вам необхідно кожного разу вручну прописуати конкретний запит — як у прикладі '(min-width: 600px)'. В разі, якщо параметри на всьому проекті зміняться — доведеться шукати по проекту усі випадки його застосування, або знову ж таки зазделегідь обгортати цей пакет в додатковий код. До того ж все це виглядає не сказати, що інтуітивно зрозуміло, тому нові люди на проекті витратять більше часу, щоб розібратись з адаптивністю.

Що натомість пропоную я:

В кастомний хук ви передаєте не рядок, а свій об’єкт, куди прописуєте одразу декілька медіа-запитів з інтуітино зрозумілими назвами, наприклад ось такий:

const myMediaQueries = {
    mobile: '(max-width: 480px)',
    tablet: '(min-width: 481px) and (max-width: 767px)',
    desktop: '(min-width: 768px)',
}

І хук повертає вам також готовий об`єкт з відповідними ключами та значеннями true/false, який можна одразу зручно розпарсити:

// [object Object] 
{
  "mobile": true,
  "tablet": false,
  "desktop": false
}
export const Header = () => {

  const { mobile, tablet, desktop } = reactMatchMedia(myMediaQueries);

  return mobile ? <div>Hello, Mobile World<div> : <div>Hello, World</div>

};

Ба більше (!), якщо вам знадобиться прибрати чи додати один з параметрів, достатньо це зробити в початковій об’єкті-константі myMediaQueries. На прикладі нижче ми додаємо в наш проект великі екрани:

const myMediaQueries = {
    mobile: '(max-width: 480px)',
    tablet: '(min-width: 481px) and (max-width: 767px)',
    desktop: '(min-width: 768px) and (max-width: 1023px)',
    bigScreen: '(min-width: 1024px)',
}

Хук тепер повертає також і новий ключ, про що можна переконатись у консолі:

// [object Object] 
{
  "mobile": false,
  "tablet": false,
  "desktop": false,
  "bigScreen": true
}

Також без вагань ви можете додати в свій об’єкт запити не тільки розімірів, а й орієнтацій, наприклад:

const myMediaQueries = {
    mobile: '(max-width: 480px)',
    tablet: '(min-width: 481px) and (max-width: 767px)',
    desktop: '(min-width: 768px) and (max-width: 1023px)',
    landscape: '(orientation: landscape)',
}

Тепер, розпарсивши результат, легше проводити більш комплексні логічні операції:

export const Header = () => {

  const { mobile, landscape } = reactMatchMedia(myMediaQueries);
  if (mobile && landscape) {
    return (
      <div> 
        *для горизонтальної орієнтації*
      <div>
)};

Таким чином, завдяки цьому хуку, можна брати потрібні значення, які мають зрозумілі назви, парсити їх на початку компонента і далі з легкістю вживати де завгодно.

Під капотом

// Хук
const reactMatchMedia = (mediaQueries) => {
  
  const queries = Object.values(mediaQueries).map((query) => matchMedia(query));

  const [values, setValues] = useState(() => {
    const valuesObject = Object.keys(mediaQueries).reduce((acc, key, index) => {
      acc[key] = queries[index].matches;
      return acc;
    }, {});

    // Милиця! Для браузрених багів
    if (Object.values(valuesObject).every((val) => val === false)) {
      valuesObject.mobile = true;
    }

    return valuesObject;
  });

  useLayoutEffect(() => {
    const updateValues = () => {
      const valuesObject = Object.keys(mediaQueries).reduce(
        (acc, key, index) => {
          acc[key] = queries[index].matches;
          return acc;
        },
        {}
      );

      // Милиця! Для браузрених багів
      if (Object.values(valuesObject).every((val) => val === false)) {
        valuesObject.mobile = true;
      }

      setValues(valuesObject);
    };

    queries.forEach((query) => query.addEventListener("change", updateValues));
    return () =>
      queries.forEach((query) =>
        query.removeEventListener("change", updateValues)
      );
  }, [queries]);

  return values;
};

Власне, що тут відбувається?

Хук отримує кастомний об’єкт, використовує редуктор для отримання від matchMedia значень .matches для кожного ключа, щоб записати їх у початковий стан. Я використовую власне useState для створення стану та useLayoutEffect — для реагування на зміни.

Чому useLayoutEffect, а не звичайний useEffect? Звичайний useEffect виконується після того, як браузер оновить екран, іншими словами, після рендерингу компонентів. Це може спричинити миготіння або затримку в зміні стану компонентів, якщо значення медіа-запитів впливають на їх розміщення або вигляд.

Використання useLayoutEffect гарантує, що оновлення значень медіа-запитів буде відбуватись безпосередньо перед малюванням на екрані, що нам власне і потрібно.

У хуці є милиця. Вона створена для браузенрних багів при роботі з matchMedia. Це рідкість, але нажаль трапляється, або може трапитись в майбутньому після оновлень браузрерів. Деякі браузери в деяких випадках повертають false для всіх медіа-запитів, навіть якщо насправді одне зі значень має бути true. Для цього я в милиці встановлюю значення mobile як true примусово, щоб не зламати подальший код. Дефолтне значення може бути встановлено будь яке — най це буде desktop, якщо ваші користувачі ймовірніше заходять зі стаціонарного пристрою.

Цей хук не є легеньким, хоча б тому, що useLayoutEffect синхроний. Проте npm-пакет react-use-match-media так само під капотом використовує саме його. Мій кастомний хук важчий за react-use-match-media хіба через використання ще й редуктора, але він і приймає більш комплексні дані, а не один єдиний рядок.


Зберігайте собі, одного дня може згадаєте мене добрим словом! А я постараюсь писати тут частіше, хоча це не дуже й просто влітку, коли собаки просять гуляти з ними, а не сидіти за компом:)

Поділись своїми ідеями в новій публікації.
Ми чекаємо саме на твій довгочит!
Марта Ярчук
Марта Ярчук@yarchuk

295Прочитань
38Автори
39Читачі
На Друкарні з 18 квітня

Більше від автора

Вам також сподобається

Коментарі (2)

Цікавий матеріал, але поправте назву хука, щоб по конвенціях була - починалася з use, будь ласка

Вам також сподобається