Друкарня від WE.UA

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

3Довгочити
381Перегляди
39Підписники
На Друкарні з 18 квітня 2023

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

Це також може зацікавити:

  • Escape Road 3

    I really enjoy playing Escape Road 3 because every chase feels thrilling and unpredictable. Dodging traffic, escaping police cars, and surviving longer each run keeps the gameplay exciting. It’s one of the best browser driving games for fans of action-packed racing adventures!

    Теми цього довгочиту:

    Frontend
  • Чому ви повинні забути про rgb() та hex кольори при роботі з вебсайтами

    Формат задання кольорів функцією rgb() було представлено ще у 1996 році. Очевидно, що CSS далеко розвинувся з тих часів. Попри це, чомусь, багато веброзробників та вебдизайнерів вперто продовжують використовувати цей застарілий, обмежений та нелюдський формат кольору по цей день.

    Теми цього довгочиту:

    Css

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

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

Це також може зацікавити:

  • Escape Road 3

    I really enjoy playing Escape Road 3 because every chase feels thrilling and unpredictable. Dodging traffic, escaping police cars, and surviving longer each run keeps the gameplay exciting. It’s one of the best browser driving games for fans of action-packed racing adventures!

    Теми цього довгочиту:

    Frontend
  • Чому ви повинні забути про rgb() та hex кольори при роботі з вебсайтами

    Формат задання кольорів функцією rgb() було представлено ще у 1996 році. Очевидно, що CSS далеко розвинувся з тих часів. Попри це, чомусь, багато веброзробників та вебдизайнерів вперто продовжують використовувати цей застарілий, обмежений та нелюдський формат кольору по цей день.

    Теми цього довгочиту:

    Css