Перевод статьи How to Build Your Own React Hooks: A Step-by-Step Guide.
Пользовательские хуки React – чрезвычайно полезный инструмент, который позволяет добавлять особые возможности и уникальные функции в ваши компоненты и приложения React.
В большинстве случаев, если вы хотите добавить дополнительную функциональность в свое приложение, вы можете просто установить стороннюю библиотеку, которая предназначена для решения определенной задачи. Но если такой библиотеки или хука не существует, что делать?
Поэтому разработчику, использующему React, для того, чтобы добавить недостающий функционал в свои проекты, важно детально изучить процесс создания пользовательских хуков. Этот механизм, реализованный в ядре библиотеки React, предоставляет оптимальный подход к решению различных задач, не поддерживаемых стандартными средствами этой библиотеки.
В этом пошаговом руководстве я покажу вам, как создать свои собственные хуки React, разобрав подробно примеры хуков, которые я разработал для своих приложений. Также я опишу саму проблему и формальную постановку задачи, для решения которых эти хуки были разработаны.
Хотите узнать, как создавать собственные хуки React при создании крутых реальных приложений? Переходите по ссылке React Bootcamp .
Содержание
- 1. Хук useCopyToClipboard
- Разрабатываем функцию handleCopy
- Добавляем интервал сброса состояния кнопки копирования
- Итог
- 2. Хук usePageBottom
- 3. Хук useWindowSize
- Разработка хука
- Как получить ширину и высоту окна
- Как добавить поддержку SSR (серверного рендеринга)
- Как удалить «прослушиватель» события изменения размера окна
- Итог
- 4. Хук useDeviceDetect
- Работа над кодом хука
- Как получить информацию о пользовательском агенте user agent
- Как определить, соответствует ли значение userAgent мобильному устройству
- Итог
- Заключение
1. Хук useCopyToClipboard
В предыдущей версии моего сайта reedbarger.com я разрешал пользователям копировать код из моих статей с помощью функции, предоставляемой пакетом react-copy-to-clipboard
.
Разберемся, как это работает. Пользователь наводит указатель мыши на блок с фрагментом кода, нажимает на кнопку для отправки в буфер обмена, и его содержимое добавляется в буфер обмена компьютера. В дальнейшем пользователь может вставить скопированный код в любой редактор или использовать по своему усмотрению.
Однако вместо того, чтобы использовать стороннюю библиотеку, я захотел воссоздать эту функциональность с помощью моего собственного хука React. Как и в случае с любым создаваемым хуком, я помещаю файл с его кодом в специальную папку, обычно называемую utils
или lib
. Далее она используется для хранения файлов с разработанными мною функциями, которые я повторно использую в своем проекте.
Код с новым хуком будет находится в файле useCopyToClipboard.js, в который я также помещу функцию с тем же именем.
Существует несколько различных способов реализовать копирование текста в буфер обмена пользователя. И ранее для этого, я обычно предпочитал использовать проверенную библиотеку из пакета copy-to-clipboard
, что делает процесс разработки более быстрым и надежным.
В следующем примере кода мы экспортируем из пакета функцию copy
, которую далее при необходимости будем вызывать в своем коде.
// utils/useCopyToClipboard.js import React from "react"; import copy from "copy-to-clipboard"; export default function useCopyToClipboard() {}
Теперь создадим новую функцию, которая будет использоваться для копирования текстового содержимого, которое будем добавлять в буфер обмена пользователя, и назовем ее handleCopy
.
Разрабатываем функцию handleCopy
Рассмотрим последовательно работу этой функции. Первое, что надо сделать – убедиться, что функция принимает в качестве аргументов данные только строкового или числового типа. Для валидации входящих данных и соответствующей их обработки будем использовать блоки кода с инструкцией if-else
. В них будем осуществлять проверку следующих типов: строка string
или число number
. В противном случае будем выводить в консоли сообщение об ошибке, которое информирует пользователя, что он не может копировать другие типы данных.
import React from "react"; import copy from "copy-to-clipboard"; export default function useCopyToClipboard() { const [isCopied, setCopied] = React.useState(false); function handleCopy(text) { if (typeof text === "string" || typeof text == "number") { // copy } else { // don't copy console.error( `Cannot copy typeof ${typeof text} to clipboard, must be a string or number.` ); } } }
После проверки типа, полученное содержимое и преобразуем в строку, которую передаем в импортируемую функцию copy
. Затем мы возвращаем функцию handleCopy
из хука в любое место нашего приложения.
Функция handleCopy
будет связана с обработчиком onClick
для соответствующих кнопок.
import React from "react"; import copy from "copy-to-clipboard"; export default function useCopyToClipboard() { function handleCopy(text) { if (typeof text === "string" || typeof text == "number") { copy(text.toString()); } else { console.error( `Cannot copy typeof ${typeof text} to clipboard, must be a string or number.` ); } } return handleCopy; }
Но кроме того, нам нужно сохранять состояние компонента, которое будет содержать информацию, был ли скопирован текст или нет. Чтобы реализовать это, в верхней части нашего хука мы будем вызывать метод useState
, и таким образом создадим новую переменную состояния isCopied
, для изменения значения которой будет вызываться функция-сеттер setCopy
.
Начальное значение переменной isCopied
будет задано логическим значением false
. Если текст был успешно скопирован, то после вызова функции copy
установим значение isCopied
равным true
. В противном случае – false
.
И наконец, мы будем возвращать из функции хука массив, содержащий переменную состояния isCopied
вместе с функцией handleCopy
.
import React from "react"; import copy from "copy-to-clipboard"; export default function useCopyToClipboard(resetInterval = null) { const [isCopied, setCopied] = React.useState(false); function handleCopy(text) { if (typeof text === "string" || typeof text == "number") { copy(text.toString()); setCopied(true); } else { setCopied(false); console.error( `Cannot copy typeof ${typeof text} to clipboard, must be a string or number.` ); } } return [isCopied, handleCopy]; }
Отлично, теперь мы можем использовать хук useCopyToClipboard
в любом компоненте, где нам это необходимо.
В моем случае я буду использовать его с компонентом кнопки копирования в буфер обмена, который получает содержимое снипета кода из специального блока на странице.
Чтобы этот код заработал необходимо добавить обработчик клика мыши для нужной кнопки. При клике на кнопке, содержимое блока в текстовом формате будет передано в буфер обмена. При успешном копировании, переменной isCopied
будет передано значение true
. Соответственно мы можем показать другой значок иконки, обозначающий, что операция копирования была успешна.
import React from "react"; import ClipboardIcon from "../svg/ClipboardIcon"; import SuccessIcon from "../svg/SuccessIcon"; import useCopyToClipboard from "../utils/useCopyToClipboard"; function CopyButton({ code }) { const [isCopied, handleCopy] = useCopyToClipboard(); return ( <button onClick={() => handleCopy(code)}> {isCopied ? <SuccessIcon /> : <ClipboardIcon />} </button> ); }
Добавляем интервал сброса состояния кнопки копирования
Как мы уже говорили, после успешного копирования текста с помощью нашего хука значение переменной isCopied
становится истинным (и таким впоследствии остается). Это означает, что мы и далее будем постоянно видеть значок успешного копирования.
Если мы захотим сбрасывать состояние кнопки копирования через определенное количество секунд в исходное состояние, то можем передать соответствующий временной интервал в хук useCopyToClipboard
. Давайте добавим ему эту полезную функциональность.
Возвращаясь к коду, добавим для функции вызова хука соответствующий параметр resetInterval
, с начальным значением null
, это гарантирует, что состояние компонента кнопки по умолчанию не будет сбрасываться.
Добавим стандартный хук useEffect
, в который передадим значение интервала сброса состояния resetInterval
и переменную isCopied
. После успешного копирования и передачи соответствующего значения переменной isCopied
, мы запускаем функцию-таймер setTimeout
с временем срабатывания равным интервалу сброса, после чего возвращаем переменной isCopied
значение false
.
Кроме того, необходимо «очистить» созданный объект тайм-аута, для случая если компонент, в котором используется наш хук, размонтируется (то есть состояние для обновления вида кнопки больше хранить не надо).
import React from "react"; import copy from "copy-to-clipboard"; export default function useCopyToClipboard(resetInterval = null) { const [isCopied, setCopied] = React.useState(false); const handleCopy = React.useCallback((text) => { if (typeof text === "string" || typeof text == "number") { copy(text.toString()); setCopied(true); } else { setCopied(false); console.error( `Cannot copy typeof ${typeof text} to clipboard, must be a string or number.` ); } }, []); React.useEffect(() => { let timeout; if (isCopied && resetInterval) { timeout = setTimeout(() => setCopied(false), resetInterval); } return () => { clearTimeout(timeout); }; }, [isCopied, resetInterval]); return [isCopied, handleCopy]; }
И, наконец, мы можем сделать последнее усовершенствование: обернуть функцию handleCopy
в хук useCallback
для того , чтобы убедиться , что он не будет создаваться заново каждый раз, когда наш компонент повторно рендерится.
Итог
И так теперь у нас есть финальная версия кода нашего хука, который теперь позволяет сбрасывать свое состояние через заданный интервал времени. Если мы передадим ему определенное значение времени, то должны увидеть результат, подобный приведенному ниже.
import React from "react"; import ClipboardIcon from "../svg/ClipboardIcon"; import SuccessIcon from "../svg/SuccessIcon"; import useCopyToClipboard from "../utils/useCopyToClipboard"; function CopyButton({ code }) { // isCopied is reset after 3 second timeout const [isCopied, handleCopy] = useCopyToClipboard(3000); return ( <button onClick={() => handleCopy(code)}> {isCopied ? <SuccessIcon /> : <ClipboardIcon />} </button> ); }
2. Хук usePageBottom
В приложениях React в некоторых случаях важно знать, когда пользователь прокрутил страницу до конца.
Так например в Instagram реализована «бесконечная «прокрутка, то есть если пользователь попадает в нижнюю часть страницы, то подгружаются и отображаются новые сообщения.
Давайте разберемся на конкретном примере, как создать свой хук usePageBottom, для использования в подобных случаях, например, для реализации «бесконечной» прокрутки.
Как и ранее, начнем с создания в нашей папке utils отдельного файла usePageBottom.js в который добавим функцию (хук) с тем же именем:
// utils/usePageBottom.js import React from "react"; export default function usePageBottom() {}
И так, нам необходимо определить момент, когда пользователь попадет в нижнюю часть страницы. Мы можем определить его произведя несложные расчеты, используя информацию, полученную из свойств глобального объекта window
. Отметим, что перед тем как обращаться к нему, нам нужно убедиться, что компонент, в котором вызывается хук, смонтирован. Для этого мы будем использовать хук useEffect
, в который передадим пустой массив зависимостей.
// utils/usePageBottom.js import React from "react"; export default function usePageBottom() { React.useEffect(() => {}, []); }
Пользователь будет прокручивать страницу до конца, то есть до тех пор пока значение свойства window.innerHeight
плюс значение свойства document.scrollTop
не станет равным document.offsetHeight
. Таким образом, если результат проверки этого условия будет истинным, то будем считать пользователь прокрутил страницу до конца:
// utils/usePageBottom.js import React from "react"; export default function usePageBottom() { React.useEffect(() => { window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight; }, []); }
Сохраним результат этого выражения в переменной isBottom
, а затем обновим переменную состояния bottom
, которую мы в конечном итоге вернем из нашего хука.
// utils/usePageBottom.js import React from "react"; export default function usePageBottom() { const [bottom, setBottom] = React.useState(false); React.useEffect(() => { const isBottom = window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight; setBottom(isButton); }, []); return bottom; }
Вот так будет в результате выглядеть наш код. Однако в таком виде он работать не будет. Давайте разберемся почему так происходит.
Проблема заключается в том, что нам нужно производить вычисления isBottom
всякий раз, когда пользователь прокручивает страницу. Это является результатом того, что мы будем постоянно «прослушивать» событие прокрутки окна с помощью метода window.addEventListener
. То есть мы будем пересчитывать это выражение, в созданной для этого локальной функции handleScroll
, которая будет вызываться всякий раз, когда пользователь прокручивает страницу.
// utils/usePageBottom.js import React from "react"; export default function usePageBottom() { const [bottom, setBottom] = React.useState(false); React.useEffect(() => { function handleScroll() { const isBottom = window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight; setBottom(isButton); } window.addEventListener("scroll", handleScroll); }, []); return bottom; }
И наконец, поскольку у нас устанавливается обработчик события скролла окна браузера, то нужно предусмотреть ситуацию, когда пользователь уходит со страницы или же наш компонент удаляется. В этом случае необходимо удалять уже не нужный обработчик событий прокрутки, чтобы его код не попытался обновить переменную состояния компонента, которая больше не существует, что приведет к появлению ошибки.
Разрешить эту проблему можно возвращая из хука useEffect
функцию, в которую обернем метод window.removeEventListener
и передадим ему ссылку на функцию handleScroll
.
// utils/usePageBottom.js import React from "react"; export default function usePageBottom() { const [bottom, setBottom] = React.useState(false); React.useEffect(() => { function handleScroll() { const isBottom = window.innerHeight + document.documentElement.scrollTop === document.documentElement.offsetHeight; setBottom(isButton); } window.addEventListener("scroll", handleScroll); return () => { window.removeEventListener("scroll", handleScroll); }; }, []); return bottom; }
Теперь мы можем просто вызвать наш хук в любом месте кода, если захотим узнать, достигли ли мы нижней части страницы или нет.
3. Хук useWindowSize
На моем сайте, реализованном на Gatsby, по мере уменьшения ширины страницы я хочу управлять отображением ссылок в меню, которое находится в верхнем header блоке.
Для этого мы могли бы в файле с разметкой JSX использовать медиа-запрос CSS или пользовательский хук React, предоставляемый сторонней библиотекой, чтобы в зависимости от текущего размера страницы скрывать или показывать ее элементы.
Раньше я использовал хук, импортируемый из библиотеки react-use
, но вместо того, чтобы каждый раз подключать целую библиотеку, я решил создать свой собственный. Этот хук, который я назвал useWindowSize
, при необходимости вычислял бы размеры окна, то есть его ширину и высоту и возвращал эти значения в нужном месте кода.
Разработка хука
Для начала в нашей папке utils создадим новый js файл с тем же именем, что и название хука useWindowSize
. Затем я импортирую React, для того чтобы использовать стандартные хуки в коде нашего хука и его корректного экспорта.
// utils/useWindowSize.js import React from "react"; export default function useWindowSize() {}
И так, поскольку я хочу использовать наш хук на сайте, работающем на Gatsby с серверным рендерингом страниц, то мне нужно как-то получить размер окна браузера пользователя. Но у нас нет и не может быть непосредственно к нему доступа, потому что мы осуществляем рендер страниц в конечном виде на сервере.
То есть для корректной работы хука нам необходимо предусмотреть тот факт, что наш код может работает как в браузере, так и на сервере. Для его определения мы можем проверить соответствие результата выполнения выражения typeof window
и строки undefined
.
В случае для браузера по умолчанию, мы можем возвращать из функции хука объект, содержащий свойства с фиксированными значениями ширины и высоты, допустим, 1200 и 800 пикселей:
// utils/useWindowSize.js import React from "react"; export default function useWindowSize() { if (typeof window !== "undefined") { return { width: 1200, height: 800 }; } }
Как получить ширину и высоту окна
И так, если наш код выполняется у пользователя в браузере и мы можем непосредственно получить ссылку на объект window
, то воспользуемся стандартным хуком useEffect
. В этом случае мы можем использовать его побочный эффект и взаимодействовать с объектом window
напрямую. Включим пустой массив в качестве зависимостей для его выполнения, чтобы гарантировать, что функция эффекта будет вызывается только один раз после того, как компонент (в котором вызывается хук) будет смонтирован.
Чтобы при необходимости узнать ширину и высоту окна браузера, мы можем добавить соответствующий «прослушиватель» события. Например, «прослушивать» событие resize
, и всякий раз, когда размеры окна браузера будут изменяться, обновлять часть состояния компонента. А точнее значение переменной windowSize
, создаваемой с помощью хука useState
, используя для этого функцию-сеттер setWindowSize
.
// utils/useWindowSize.js import React from "react"; export default function useWindowSize() { if (typeof window !== "undefined") { return { width: 1200, height: 800 }; } const [windowSize, setWindowSize] = React.useState(); React.useEffect(() => { window.addEventListener("resize", () => { setWindowSize({ width: window.innerWidth, height: window.innerHeight }); }); }, []); }
При изменении размера окна будет вызвана функция обратного вызова, и состояние компонента, а точнее значение переменной windowSize
будет обновлено с учетом текущих размеров окна браузера. Для этого используются значения ширины и высоты из объекта window.innerWidth
и window.innerHeight
.
Как добавить поддержку SSR (серверного рендеринга)
Тем не менее с использованием технологии SSR, код в том виде работать не будет. Это связано с тем, что ключевое правило хуков состоит в том, что их нельзя вызывать при необходимости в зависимости от выполнения каких-либо условий. Как результат этого ограничения, мы не можем для их проверки использовать условные операторы до вызова хуков useState
, либо useEffect
.
Итак, чтобы исправить это, мы отдельно установим начальное значение переменной, использующейся с хуком useState
, в зависимости от результатов проверки определенных условий. Для этого создадим переменную isSSR
, в которую будем передавать результат выполнения логического выражения, которое проверяет соответствие типа объекта window
строке undefined
.
Далее мы будем использовать тернарный оператор для установки значений ширины и высоты, предварительно проверив, находимся ли мы на сервере. Если это так, мы будем использовать значение по умолчанию, а если нет, мы будем использовать значения из объекта window.innerWidth
и window.innerHeight
.
// utils/useWindowSize.js import React from "react"; export default function useWindowSize() { // if (typeof window !== "undefined") { // return { width: 1200, height: 800 }; // } const isSSR = typeof window !== "undefined"; const [windowSize, setWindowSize] = React.useState({ width: isSSR ? 1200 : window.innerWidth, height: isSSR ? 800 : window.innerHeight, }); React.useEffect(() => { window.addEventListener("resize", () => { setWindowSize({ width: window.innerWidth, height: window.innerHeight }); }); }, []); }
Затем, наконец, нам нужно подумать о том, что будет когда наш компонент будет отмонтированы. В этом случае нам нужно будет удалять «прослушиватель» события изменения размера окна.
Как удалить «прослушиватель» события изменения размера окна
Вы можете сделать это, вернув соответствующую функцию из хука useEffect
. То есть, мы можем удалить «прослушиватель» используя метод window.removeEventListener
следующим образом.
// utils/useWindowSize.js import React from "react"; export default function useWindowSize() { // if (typeof window !== "undefined") { // return { width: 1200, height: 800 }; // } const isSSR = typeof window !== "undefined"; const [windowSize, setWindowSize] = React.useState({ width: isSSR ? 1200 : window.innerWidth, height: isSSR ? 800 : window.innerHeight, }); React.useEffect(() => { window.addEventListener("resize", () => { setWindowSize({ width: window.innerWidth, height: window.innerHeight }); }); return () => { window.removeEventListener("resize", () => { setWindowSize({ width: window.innerWidth, height: window.innerHeight }); }); }; }, []); }
Код получился не очень функциональным. И для того, чтобы это исправить мы будем использовать ссылку на одну функцию, а не на две разные, как в нашем коде. Для этого создадим отдельно функцию changeWindowSize
, которую будем использовать для обратного вызова для в обоих случаях.
И наконец, в конце кода хука вернем значение переменной состояния windowSize
.
// utils/useWindowSize.js import React from "react"; export default function useWindowSize() { const isSSR = typeof window !== "undefined"; const [windowSize, setWindowSize] = React.useState({ width: isSSR ? 1200 : window.innerWidth, height: isSSR ? 800 : window.innerHeight, }); function changeWindowSize() { setWindowSize({ width: window.innerWidth, height: window.innerHeight }); } React.useEffect(() => { window.addEventListener("resize", changeWindowSize); return () => { window.removeEventListener("resize", changeWindowSize); }; }, []); return windowSize; }
Итог
Чтобы использовать наш хук, нужно импортировать его, а затем при необходимости вызывать и использовать полученные значения размеров окна по своему усмотрению. Например, в зависимости от ширины окна скрывать или показывать определенные элементы страницы.
Примем за фиксированное значение ширины отметку в 500 пикселей. При изменении размеров окна менее этого значения – скрываем все ссылки в меню и показываем только кнопку «Join Now», как в примере выше:
// components/StickyHeader.js import React from "react"; import useWindowSize from "../utils/useWindowSize"; function StickyHeader() { const { width } = useWindowSize(); return ( <div> {/* visible only when window greater than 500px */} {width > 500 && ( <> <div onClick={onTestimonialsClick} role="button"> <span>Testimonials</span> </div> <div onClick={onPriceClick} role="button"> <span>Price</span> </div> <div> <span onClick={onQuestionClick} role="button"> Question? </span> </div> </> )} {/* visible at any window size */} <div> <span className="primary-button" onClick={onPriceClick} role="button"> Join Now </span> </div> </div> ); }
Этот хук будет также корректно работать с любым серверным приложением React, таким как Gatsby и Next.js.
4. Хук useDeviceDetect
Я создал лендинг для продвижения своего нового курса, и у меня возникла очень странная ошибка на мобильных устройствах. На настольных компьютерах стили сайта выглядели великолепно. Но когда я посмотрел на мобильную версию, то увидел, что всё сломалось и сайт выглядел совсем не так, как я задумал.
Я отследил причину проблемы до одной сторонней библиотеки под названием response-device-detect
, которую я использовал, чтобы определять, использует ли пользователь для просмотра сайта мобильное устройство или нет. И если это так, я бы убирал слишком большой заголовок из-за которого ломалась страница.
// templates/course.js import React from "react"; import { isMobile } from "react-device-detect"; function Course() { return ( <> <SEO /> {!isMobile && <StickyHeader {...courseData} />} {/* more components... */} </> ); }
Проблема заключалась в том, что эта библиотека не поддерживает рендеринг страниц на стороне сервера, который Gatsby использует по умолчанию. Поэтому мне пришлось создать собственное решение, для проверки, когда пользователь пользуется мобильным устройством. И для этого я написал хук, который назвал useDeviceDetect
.
Работа над кодом хука
Как и ранее я создал в моей папке utils
отдельный файл с тем же именем, useDeviceDetect.js. И поскольку хуки – это просто функции JavaScript для общего назначения, которые используются механизмом хуков в React, я создаю функцию с тем же именем useDeviceDetect
и импортирую React.
// utils/useDeviceDetect.js import React from "react"; export default function useDeviceDetect() {}
Как получить информацию о пользовательском агенте user agent
Способ, которым мы воспользуемся, чтобы определить тип устройства, которое использует пользователь – это получить и проанализировать значение свойства userAgent
, которое в свою очередь содержится в свойстве navigator
глобального объекта окна window
.
А поскольку взаимодействие с интерфейсом окна браузера window API, как и с API внешнего ресурса можно в общем случае рассматривать как побочный эффект, то мы можем получить доступ к информации о пользовательском агенте внутри кода хука useEffect
.
// utils/useDeviceDetect.js import React from "react"; export default function useDeviceDetect() { React.useEffect(() => { console.log(`user's device is: ${window.navigator.userAgent}`); // can also be written as 'navigator.userAgent' }, []); }
После монтирования компонента используем инструкцию кода typeof navigator
, чтобы определить, выполняется код в браузере или же на сервере. Если мы находимся на сервере, то у нас соответственно не будет доступа к объекту window
. Поэтому результат выполнения выражения typeof navigator
будет эквивалентен строке undefined
, поскольку этого свойства не существует. В противном случае (код выполняется в браузере) мы можем получить значение свойства, содержащего информацию о пользовательском агенте, способом описанном выше.
Используем тернарный оператор для получения данных userAgent
следующим образом:
// utils/useDeviceDetect.js import React from "react"; export default function useDeviceDetect() { React.useEffect(() => { const userAgent = typeof navigator === "undefined" ? "" : navigator.userAgent; }, []); }
Как определить, соответствует ли значение userAgent мобильному устройству
Значение свойства userAgent
представляет собой строку, которая содержит следующие имена устройств, соответствующие мобильным устройствам: Android, BlackBerry, iPhone, iPad, iPod, Opera Mini, IEMobile или WPDesktop.
И всё, что нам нужно сделать это, используя полученную строку и метод match()
с соответствующим регулярным выражением, проверить содержит ли она одну из перечисленных строк. Логическое значение, полученное в результате выполнения проверки сохраним в локальной переменной с именем mobile
.
Далее сохраним полученный результат в состоянии компонента, используя хук useState
, которому при вызове присвоим начальное значение false
. Для этого создадим переменную состояния isMobile
, и соответствующую ей функцию-сеттер setMobile
.
// utils/useDeviceDetect.js import React from "react"; export default function useDeviceDetect() { const [isMobile, setMobile] = React.useState(false); React.useEffect(() => { const userAgent = typeof window.navigator === "undefined" ? "" : navigator.userAgent; const mobile = Boolean( userAgent.match( /Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i ) ); setMobile(mobile); }, []); }
Теперь, как только в переменную mobile
будет передаваться значение мы соответственно будем изменять состояние компонента. И наконец, мы вернем из хука объект, чтобы в дальнейшем добавить в него больше свойств, если захотим привнести большей функциональности нашему хуку.
В возвращаемый объект добавим значение isMobile
:
// utils/useDeviceDetect.js import React from "react"; export default function useDeviceDetect() { const [isMobile, setMobile] = React.useState(false); React.useEffect(() => { const userAgent = typeof window.navigator === "undefined" ? "" : navigator.userAgent; const mobile = Boolean( userAgent.match( /Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i ) ); setMobile(mobile); }, []); return { isMobile }; }
Итог
Вернемся к нашему лендингу. Теперь, используя синтаксис деструктуризации объекта, мы можем вызывать наш хук в любом месте кода, чтобы получить значение, соответствующее типу устройства:
// templates/course.js import React from "react"; import useDeviceDetect from "../utils/useDeviceDetect"; function Course() { const { isMobile } = useDeviceDetect(); return ( <> <SEO /> {!isMobile && <StickyHeader {...courseData} />} {/* more components... */} </> ); }
Заключение
В своей статье каждым из рассмотренных примеров я попытался проиллюстрировать возможности, которые дают пользовательские хуки React для решения практических задач, когда сторонние библиотеки терпят неудачу.
Я надеюсь, что это руководство дало вам лучшее представление о том, когда и как необходимо разрабатывать свои собственные хуки React. Не стесняйтесь использовать любой из хуков, рассмотренных в этой статье и приведенный их код в своих проектах, а также в качестве источника вдохновения для написания кода ваших хуков React.
id | title | permalink | next | prev |
---|---|---|---|---|
hooks-custom |
Создание пользовательских хуков |
docs/hooks-custom.html |
hooks-reference.html |
hooks-rules.html |
Хуки — нововведение в React 16.8, которое позволяет использовать состояние и другие возможности React без написания классов.
Создание пользовательских хуков позволяет вам перенести логику компонентов в функции, которые можно повторно использовать.
В разделе использование хука эффекта мы увидели компонент из приложения чата, в котором отображается сообщение о том, находится ли наш друг в сети:
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Загрузка...';
}
return isOnline ? 'В сети' : 'Не в сети';
}
Теперь предположим, что в приложении чата также есть список контактов, и мы хотим отображать зелёным цветом имена пользователей, которые сейчас в сети. Мы могли бы просто скопировать и вставить приведённую выше логику в наш компонент FriendListItem
, но это не самый лучший вариант:
import React, { useState, useEffect } from 'react';
function FriendListItem(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
Вместо этого, мы бы хотели, чтобы FriendStatus
и FriendListItem
разделяли эту логику.
Когда одинаковую логику состояния нужно повторно использовать в нескольких компонентах, в React традиционно применялись рендер-пропсы и компоненты высшего порядка. Рассмотрим, как хуки решают многие из тех же задач, не добавляя лишних компонентов в ваше дерево.
Извлечение логики в пользовательский хук {#extracting-a-custom-hook}
Когда мы хотим, чтобы две JavaScript-функции разделяли какую-то логику, мы извлекаем её в третью функцию. И компоненты и хуки являются функциями, поэтому с ними это тоже работает!
Пользовательский хук — это JavaScript-функция, имя которой начинается с «use», и которая может вызывать другие хуки. Например, функция useFriendStatus
ниже — это наш первый пользовательский хук:
import { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
Здесь нет ничего нового, логика просто скопирована из компонентов выше. Так же как и в компонентах, убедитесь, что вы не используете другие хуки внутри условных операторов и вызываете их на верхнем уровне вашего хука.
В отличие от React-компонента, пользовательский хук не обязательно должен иметь конкретную сигнатуру. Мы можем решить, что он принимает в качестве аргументов, и должен ли он что-либо возвращать. Другими словами, всё как в обычных функциях. Имя функции-хука всегда следует начинать с use
, чтобы вы могли сразу увидеть, что к ней применяются правила хуков.
Цель нашего хука useFriendStatus
— подписать нас на статус друга. Поэтому он принимает в качестве аргумента friendID
и возвращает статус друга в сети:
function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null); // ... return isOnline; }
Теперь давайте посмотрим, как мы можем использовать наш пользовательский хук.
Использование пользовательского хука {#using-a-custom-hook}
Вначале нашей целью было удалить дублированную логику из компонентов FriendStatus
и FriendListItem
. Они оба хотят знать, есть ли друг в сети.
Теперь, когда мы извлекли эту логику в хук useFriendStatus
, мы можем его использовать:
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Загрузка...';
}
return isOnline ? 'В сети' : 'Не в сети';
}
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
Код будет работать как раньше? Да, он работает точно так же. Если вы посмотрите внимательно, вы заметите, что мы не вносили никаких изменений в логику. Всё, что мы сделали, это извлекли общий код в отдельную функцию. Пользовательские хуки — это скорее соглашение, соответствующее дизайну хуков, нежели чем возможность самого React.
Должен ли я писать «use» в начале названия хука? Очень желательно. Это важное условие, без которого мы не сможем автоматически проверять нарушения правил хуков, потому что не определим, содержит ли определённая функция вызовы хуков внутри.
У хука, используемого в двух компонентах, одинаковое состояние? Нет. Пользовательские хуки — это механизм повторного использования логики с состоянием (например, установка подписки и сохранение текущего значения), но каждый раз, когда вы используете пользовательский хук, всё состояние и эффекты внутри него полностью изолированы.
Как пользовательский хук получает изолированное состояние? Каждый вызов хука получает изолированное состояние. Поскольку мы вызываем useFriendStatus
напрямую, с точки зрения React наш компонент просто вызывает useState
и useEffect
. И как мы узнали ранее, мы можем вызывать useState
и useEffect
много раз в одном компоненте, и они будут полностью независимы.
Совет: Передача информации между хуками {#tip-pass-information-between-hooks}
Поскольку хуки являются функциями, мы можем передавать информацию между ними.
Продемонстрируем это, используя другой компонент из нашего гипотетического примера чата. Это средство выбора получателей сообщений чата, которое показывает, находится ли выбранный в данный момент друг в сети:
const friendList = [
{ id: 1, name: 'Татьяна' },
{ id: 2, name: 'Алла' },
{ id: 3, name: 'Лиля' },
];
function ChatRecipientPicker() {
const [recipientID, setRecipientID] = useState(1);
const isRecipientOnline = useFriendStatus(recipientID);
return (
<>
<Circle color={isRecipientOnline ? 'green' : 'red'} />
<select
value={recipientID}
onChange={e => setRecipientID(Number(e.target.value))}
>
{friendList.map(friend => (
<option key={friend.id} value={friend.id}>
{friend.name}
</option>
))}
</select>
</>
);
}
Мы сохраняем выбранный в настоящее время идентификатор друга в переменной состояния recipientID
и обновляем его, если пользователь выбирает другого друга в <select>
.
Поскольку вызов хука useState
даёт нам последнее значение переменной состояния recipientID
, мы можем передать его в наш пользовательский хук useFriendStatus
в качестве аргумента:
const [recipientID, setRecipientID] = useState(1); const isRecipientOnline = useFriendStatus(recipientID);
Это позволяет нам узнать, находится ли выбранный друг в сети. Если мы выберем другого друга и обновим переменную состояния recipientID
, наш хук useFriendStatus
отменит подписку на ранее выбранного друга и подпишется на статус вновь выбранного.
используйтеВоображение()
{#useyourimagination}
Пользовательские хуки предлагают гибкую логику совместного использования, которая раньше была невозможна в React-компонентах. Вы можете написать собственные хуки, которые охватывают широкий спектр вариантов использования, таких как обработка форм, анимация, декларативные подписки, таймеры и, возможно, многих других, которые мы не рассматривали. Более того, вы можете создавать хуки, которые также просты в использовании, как и встроенные функции React.
Попробуйте не добавлять абстракцию слишком рано. Теперь, когда функциональные компоненты обладают большими возможностями, вполне вероятно, средний функциональный компонент станет длиннее в вашей кодовой базе. Это нормально, не думайте, что вы должны немедленно разделить его на хуки. Но мы также рекомендуем вам находить ситуации, когда пользовательский хук поможет скрыть сложную логику за простым интерфейсом или распутать большой компонент.
Например, у вас есть сложный компонент, содержащий множество внутренних состояний, каждое из которых управляется особым образом. useState
не упрощает централизацию логики обновления, поэтому её можно попробовать переписать как Redux-редюсер:
function todosReducer(state, action) { switch (action.type) { case 'add': return [...state, { text: action.text, completed: false }]; // ... другие действия ... default: return state; } }
Редюсеры очень удобно тестировать изолированно и масштабировать для реализации сложной логики обновления. При необходимости вы можете разбить их на более мелкие редюсеры. Однако вам может нравиться пользоваться преимуществами внутреннего состояния React, или вы не хотите устанавливать ещё одну стороннюю библиотеку.
Что если мы могли бы написать хук useReducer
, который позволяет нам управлять внутренним состоянием нашего компонента с помощью редюсера? Упрощённая версия может выглядеть так:
function useReducer(reducer, initialState) { const [state, setState] = useState(initialState); function dispatch(action) { const nextState = reducer(state, action); setState(nextState); } return [state, dispatch]; }
Теперь мы можем использовать его в нашем компоненте и с помощью редюсера управлять его состоянием:
function Todos() {
const [todos, dispatch] = useReducer(todosReducer, []);
function handleAddClick(text) {
dispatch({ type: 'add', text });
}
// ...
}
Так как необходимость управления внутренним состоянием с помощью редюсера в сложном компоненте достаточно распространена, мы встроили хук useReducer
прямо в React. Вы найдёте его вместе с другими встроенными хуками в API-справочнике хуков.
Тема хуков в React разработке довольно популярна и не единожды разобрана различными специалистами. Цель нашего материала, не столько привнести что-то новое, сколько разобрать достаточно важную и актуальную тему максимально доступным и понятным языком, чтобы она не вызывала сложности даже у новичков в разработке приложений на React.
Впервые появились в React версии 16.8. Структура приложений, построенных на использовании хуков очень понравилась сообществу своей гибкостью и простотой, что позволило ей практически заместить классы. Как это сделать, рассказывает Русаков Николай, начальник отдела SEO-продвижения студии Moeseo.
С хуками проще!
Многих новичков классы отпугивали своей громоздкостью и меньшей прозрачностью. Механизмы состояния и многоразового использования выглядели значительно сложнее, чем в действительности. Введение хуков в значительной степени решило эту проблему и существенно повысило популярность библиотеки, стабильно растущую уже на протяжении нескольких лет.
React из коробки содержит несколько очень важных хуков.
useState — управляем состоянием в функциональных компонентах
Одним из важнейших хуков является useState. Его название сразу дает понять для чего он используется — он отвечает за состояние. Данный хук позволяет воздействовать на состояние подобно методу this.setState(). Для начала давайте рассмотрим простой пример его использования:
const MyComponent = () => {
const [counter, setCounter] = useState(1);
return (
<button onClick={() => setCounter(counter + 1)}>
+1 к прежнему значению
</button>
);
};
В данном примере, после объявления функционального элемента, мы вызываем метод useState, передавая ему значение по умолчанию:
const [counter, setCounter] = useState(1);
Метод возвращает массив из 2-х элементов. 1-й элемент — это переменная состояния, 2-й элемент содержит функцию для изменения этого состояния. Для кнопки установлено событие onClick, которое позволяет изменять состояние, увеличивая значение на 1 от предыдущего:
onClick={() => setCounter(counter + 1)}
useContext — передаем информацию на любые уровни вложенности
Как правило, родительский компонент делится данными с дочерними при помощи props. Однако, бывает необходимость передать эти данные не только ближайшим «детям» этого компоненты, но и более вложенным компонентам. Передавать по всей цепочке «родственников» — ужасно неудобно и может привести к ошибкам. В таком случае удобно использовать useContext.
import {createContext, useContext} from "react";
const MyContext = createContext("информация отстутствует");
const Bookcase = () =⟩ {
return (
⟨MyContext.Provider value="шкаф #1 "⟩
⟨Bookshelf /⟩
⟨/MyContext.Provider⟩
);
};
const Bookshelf = () =⟩ {
return ⟨Book /⟩;
};
const Book = () =⟩ {
const context = useContext(MyContext);
return `Книга лежит в: "${context}"`;
};
В данном примере, благодаря хуку useContext, каждая книга может иметь информацию о том, в каком шкафу она расположена, при этом нет необходимости получать ее он непосредственного родителя (книжной полки).
useEffect — реализуем функционал жизненного цикла
Используются для моделирования жизненного цикла, также как это делают методы componentDidMount, componentDidUpdate, componentWillUnmount при использовании структуры React на классах. В качестве аргументов передается callback-функция, которая выполняет необходимые действия и массив с переменными, за изменением которых необходимо наблюдать и при их изменении вызывать callback.
useRef — связываем переменные напрямую с DOM
Чтобы обратиться напрямую к DOM-элементам в функциональных компонентах необходимо использовать useRef. В примере ниже мы привязываем кнопку к переменной buttonRef и можем использовать эту связь в callback-функции, переданной в качестве 1-го аргумента в useRef.
const MyButton = () =⟩ {
const buttonRef = useRef();
useEffect(() =⟩ {
console.log(buttonRef.current.innerHTML);
}, []);
return ⟨button ref={ref}⟩Моя кнопка⟨/button⟩;
};
Больше хуков!
useReducer: позволяет хранить значение состояние независимо от вложенности, является аналогом Redux.
useMemo: позволяет хранить значение и пересчитывать, только при изменении зависимостей.
useCallback: используется для мемоизации функций, позволяет избежать лишних рендерингов и повторного создания функций. Очень полезен для оптимизации.
Пишем собственный хук
По своей сути хуки являются обычными функциями, название которых начинается с приставки «use». Они могут использовать другие хуки, принимать аргументы и возвращать результат.
Собственные хуки удобно создавать, когда требуется вынести логику, часто применяемую в разных компонентах приложения. Нужно помнить про важные ограничения для хуков: нельзя вызывать внутри условных конструкций и циклов — это вызовет ошибку в приложении.
Давайте рассмотрим пример из официальной документации react. Он интересен тем, что описывает пример создания хука useReducer с использованием уже существующего useState.
Допустим, мы работаем с компонентом, который имеет достаточно развитую логику управления состояниями в зависимости от их типа. В таком случа, useState — не очень удобно использовать при централизованной логике управления состояниями и больше подойдет Redux-reducer:
function todosReducer(state, action) {
switch (action.type) {
case 'add':
return [...state, {
text: action.text,
completed: false
}];
// ... other actions ...
default:
return state;
}
}
Наш хук будет выглядеть так (упрощенно):
function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState)
function dispatch(action) {
const nextState = reducer(state, action)
setState(nextState)
}
return [state, dispatch]
}
Здесь мы принимаем в качестве параметров — функцию-редьюсер, отвечающую за логику изменения состояния, начальное состояние, а возвращаем актуальное состояние и функцию для его изменения. При этом, мы можем очень просто модифицировать логику изменения состояний, добавив/изменив типы в функции todosReducer. После чего, удобно использовать эту логику в своем компоненте:
function Todos() {
const [todos, dispatch] = useReducer(todosReducer, [])
function handleAddClick(text) {
dispatch({ type: 'add', text })
}
// ...
}
Хуки могут использоваться для совершенно разнообразных целей. Давайте рассмотрим пример хука для работы с локальным хранилищем браузера:
function getStorageValue(key, defaultValue) {
const saved = localStorage.getItem(key);
const initial = !saved || saved === 'undefined' ? null : JSON.parse(saved);
return initial || defaultValue;
}
export default function useLocalStorage (key, defaultValue){
const [value, setValue] = useState(() =⟩ {
return getStorageValue(key, defaultValue);
});
useEffect(() =⟩ {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
};
Мы определили функцию работы с localStorage, написав собственную логику получения первичных данных из хранилища. Следом создали хук, который инициализирует состояние, используя результат выполнения данной функции. Также мы используем useEffect, чтобы при изменении значения переменных key или value, произвести запись в localStorage. В своем компоненте мы можем использовать хук таким образом:
const [storageData, setStorageData] = useLocalStorage(‘my-data’);
Так, у нас получилось связать данные в состоянии с данными в локальном хранилище браузера.
На этом пока что все, надеюсь, что данная статья помогла сделать понимание темы хуков для Вас проще!
Доброго времени суток, друзья. В данной статье мы вновь продолжим серию статей о React hooks. Учитывая количество предыдущих статей о хуках, где были уже рассмотрены все наиболее используемые хуки, вы можете задать мне следующий вопрос: «А можно ли писать свои хуки?» Конечно можно. Данная возможность является одной из самых крутых фич, которую нам дают хуки. Благодаря написанию своих собственных хуков, можно инкапсулировать большие куски кода, использовать внутри другие хуки и при этом пользоваться простотой и гибкостью, которою предоставили нам хуки.
Видео тут:
Исходный код тут
Как написать свой хук?
Давайте вспомним базовые правила. Любой хук – это функция, имеющая в начале своего наименования «use» и далее включающая название хука, предопределяющего непосредственно его функциональность (например, useState, useEffect и тд). В теле функции описывается логика функции, при этом хук обязательно должен возвращать результаты выполнения в виде значений или функций. С теорией достаточно, давайте перейдем к практике и напишем пару хуков.
Для начала поработаем с формами. Представьте, что у нас есть форма с одним полем ввода.
Пример (App.js):
import {useState} from 'react'
function App() {
const [value, setValue] = useState('')
return (
<div className="App">
<form>
<input
value={value}
onChange={e => setValue(e.target.value)}
/>
</div>
</form>
);
}
export default App;
Давайте напишем хук useInput, который позволит скрыть всю логику работы с состоянием input и удобно масштабировать ее.
Пример (useInput.js):
import { useState } from "react";
const useInput = (initial, required) => {
const [value, setValue] = useState(initial);
const [error, setError] = useState(null);
return {
value,
onBlur: e => {
if (!e.target.value && required) setError("Required field");
else setError(null);
},
onChange: e => setValue(e.target.value),
error
};
};
export default useInput
В данном хуке, помимо возвращаемого значения и функций изменения, на событие onBlur навешена логика обязательного/ необязательного поля с возможностью передачи флага required вторым параметром хука. Ошибка будет храниться отдельным состоянием и ее также можно будет легко кастомизировать. Теперь немного поправим App.js, куда импортируем хук useInput.
Пример:
import useInput from './hooks/useInput'
function App() {
const name = useInput('hello world', true)
return (
<div className="App">
<form>
<input {...name}/>
{name.error && <span style={{ color: 'red'}}>{name.error}</span>}
</form>
</div>
);
}
export default App;
Как видите данный хук прекрасно работает. Если поле ввода будет пустым и не в фокусе, то возникнет предупреждение, что данное поле обязательно должно быть заполнено.
Пишем хук useFetch
Данный хук предназначен для взаимодействия с сервером путем отправления запросов и возвращения нужных ответов для дальнейшего рендеринга. Давайте посмотрим на код хука
Пример (useFetch.js):
import { useState, useEffect } from "react";
const useFetch = (url, options) => {
const [status, setStatus] = useState({
loading: false,
data: undefined,
error: undefined
});
function fetchNow(url, options) {
setStatus({ loading: true });
fetch(url, options)
.then((res) => res.json())
.then((res) => {
setStatus({ loading: false, data: res.data });
})
.catch((error) => {
setStatus({ loading: false, error });
});
}
useEffect(() => {
if (url) {
fetchNow(url, options);
}
}, []);
return { ...status, fetchNow };
}
export default useFetch
Хук принимает в себя два параметра, первый из которых «url», куда будет происходить вызов запроса, и «options», в который можно передать дополнительные параметры стандартной функции fetch.
Хук возвращает функцию fetchNow и все состояния, которые он хранит в объекте status.
Пример (App.js):
import useInput from './hooks/useInput'
import useFetch from './hooks/useFetch'
function App() {
const name = useInput('hello wordl', true)
const { data, loading, error } = useFetch('https://www.reddit.com/r/news.json')
if(loading) return 'Loading ...'
if(error) {
console.log("error", error);
return null;
}
return (
<div className="App">
<form>
<input {...name}/>
{name.error && <span style={{ color: 'red'}}>{name.error}</span>}
</form>
{JSON.stringify(data && data.dist)}
</div>
);
}
export default App;
Как видите, для демонстрации работы хука, в useFecth был передан url сервера www.reddit.com. Данный хук выполняется сразу после рендеринга компонента, этот вызов можно ограничить и заменить на вызов по событию (клик кнопки, скрол и тд.), используя функции fetchNow.
Пишем хук useTheme
И последний кастомный хук, который мы рассмотрим в данной статье является useTheme, основная задача которого – изменение цвета приложения.
Пример (useTheme.js):
const useTheme = () => {
const [theme, setTheme] = useState("light");
const toggleTheme = () => {
if (theme !== "dark") {
localStorage.setItem("theme", "dark");
setTheme("dark");
} else {
localStorage.setItem("theme", "light");
setTheme("light");
}
};
useEffect(() => {
const localTheme = localStorage.getItem("theme");
if (localTheme) {
setTheme(localTheme);
}
}, []);
return {
theme,
toggleTheme
};
};
export default useTheme
Хук хранит первоначальное состояние в localStorage и затем помещает его в useState. Если тема отсутствует в localStorage, то берётся дефолтное значение «light»
Пример (App.js):
import useInput from './hooks/useInput'
import useFetch from './hooks/useFetch'
import useTheme from './hooks/useTheme'
import './App.css'
function App() {
const name = useInput('hello wordl', true)
const { data, loading, error } = useFetch('https://www.reddit.com/r/news.json')
const { theme, toggleTheme } = useTheme();
if(loading) return 'Loading ...'
if(error) {
console.log("error", error);
return null;
}
return (
<div className={`App ${theme}`}>
<button type="button" onClick={toggleTheme}>
Switch theme
</button>
<form>
<input {...name}/>
{name.error && <span style={{ color: 'red'}}>{name.error}</span>}
</form>
{JSON.stringify(data && data.dist)}
</div>
);
}
export default App;
Чтобы данный хук заработал, не забудьте добавить стили для оформления темы.
.App.dark {
background-color: #000;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
Заключение
Сегодня мы рассмотрели понятие Custom Hooks в React, поговорили о теории хуков, а также научились создавать свои хуки (useInput, useFetch, useTheme). Надеюсь, что данный материал был вам полезен. Учитесь, думайте, пишите код. Удачного кодинга, друзья!
Подписывайтесь на наш канал в Telegram и на YouTube для получения самой последней и актуальной информации.