Готовий кастомний хук. Достатньо скопіювати — і матимете зручну можливість отримувати дані від matchMedia, задавати кастомні медіа-запити, легко їх змінювати та використовувати все це у React.
У попередній статті про різноманітні підходи до роботи над адаптивністю веб-сайтів я згадувала matchMedia — JavaScript API, який дозволяє отримувати відповіді true/false на будь-який ваш медіа-запит та керувати кодом в залежності від розміру екрану. Це напрочуд крутий інструмент в разі якщо у вашому проекті багато елементів, що мають реагувати на зміни екрану у корстувача. Проте його не можна використовувати за просто так у React. Як спеціаістка, що найбільше працює саме з рекатом, я написала кастомний хук, яким ділюся з вами.
А тепер по порядку
В статті я розказую про:
npm-пакет для роботи з matchMedia, який вже існує
плюси свого хука
що відбувається під капотом
На 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 хіба через використання ще й редуктора, але він і приймає більш комплексні дані, а не один єдиний рядок.
Зберігайте собі, одного дня може згадаєте мене добрим словом! А я постараюсь писати тут частіше, хоча це не дуже й просто влітку, коли собаки просять гуляти з ними, а не сидіти за компом:)