От автора: с появлением React Hooks количество совместно используемого кода в кодовых базах React резко возросло. Поскольку хуки — это тонкие API-интерфейсы поверх React, разработчики могут сотрудничать, прикрепляя повторно используемое поведение к компонентам и разделяя это поведение на более мелкие модули.
Хотя это похоже на то, как разработчики JavaScript абстрагируют бизнес-логику в модулях ванилла JavaScript, хуки предоставляют больше, чем просто функции JavaScript. Вместо того, чтобы вводить и выводить данные, разработчики могут расширить спектр возможностей того, что может происходить внутри хука.
Например, разработчики могут:
Мутировать и управлять частью состояния для определенного компонента или всего приложения
Запускать дополнительные эффекты на странице, например изменение заголовка вкладки браузера.
Исправлять внешние API, подключившись к жизненному циклу компонентов React с помощью хуков.
В этом посте мы рассмотрим последнюю возможность. В качестве примера мы абстрагируем API MutationObserver в пользовательском React Hook, демонстрируя, как мы можем создавать надежные, совместно используемые фрагменты логики в базе кода React.
Мы создадим динамическое поле label, которое обновляется, чтобы указать, сколько элементов у нас в списке. Вместо использования предоставленного массива элементов состояния React мы будем использовать API MutationObserver для обнаружения добавленных элементов и соответствующего обновления значения label.
Обновление динамического значения, чтобы подсчитать количество фруктов в списке.
Обзор реализации
Следующий код представляет собой простой компонент, отображающий наш список. Он также обновляет значение счетчика, которое представляет количество фруктов в списке в данный момент:
export default function App() { const listRef = useRef(); const [count, setCount] = useState(2); const [fruits, setFruits] = useState(["apple", "peach"]); const onListMutation = useCallback( (mutationList) => { setCount(mutationList[0].target.children.length); }, [setCount] ); useMutationObservable(listRef.current, onListMutation); return ( <div> <span>{`Added ${count} fruits`}</span> <br /> <button onClick={() => setFruits([...fruits, `random fruit ${fruits.length}`])} > Add random fruit </button> <ul ref={listRef}> {fruits.map((f) => ( <li key={f}>{f}</li> ))} </ul> </div> ); }
Мы хотим запускать функцию обратного вызова всякий раз, когда изменяется наш элемент списка. В обратном вызове, на который мы ссылаемся, дочерние элементы сообщают нам количество элементов в списке.
Реализация пользовательского хука useMutationObservable
Давайте посмотрим на точку интеграции:
useMutationObservable(listRef.current, onListMutation);
Вышеупомянутый пользовательский хук useMutationObservable абстрагирует необходимые операции для наблюдения за изменениями в элементе, переданным в качестве первого параметра. Затем он запускает обратный вызов, переданный в качестве второго параметра, всякий раз, когда целевой элемент изменяется. Теперь давайте реализуем наш хук useMutationObservable.
В хуке есть несколько шаблонных операций, которые необходимо понять. Во-первых, мы должны предоставить набор параметров, соответствующих API MutationObserver.
После создания экземпляра MutationObserver мы должны вызвать observe, чтобы отслеживать изменения в целевом элементе DOM.
Когда нам больше не нужно отслеживать изменения, мы должны вызвать disconnect для наблюдателя. Это должно произойти, когда компонент приложения отключается:
const DEFAULT_OPTIONS = { config: { attributes: true, childList: true, subtree: true }, }; function useMutationObservable(targetEl, cb, options = DEFAULT_OPTIONS) { const [observer, setObserver] = useState(null); useEffect(() => { const obs = new MutationObserver(cb); setObserver(obs); }, [cb, options, setObserver]); useEffect(() => { if (!observer) return; const { config } = options; observer.observe(targetEl, config); return () => { if (observer) { observer.disconnect(); } }; }, [observer, targetEl, options]); }
Вся вышеупомянутая работа, включая инициализацию MutationObserver с правильными параметрами, наблюдение за изменениями с помощью вызова Observer.observe и отключение с помощью Observer.disconnect, абстрагируется от клиента.
Мы не только экспортируем функциональность, но и выполняем очистку, подключаясь к жизненному циклу компонентов React и используя обратные вызовы очистки для уничтожения экземпляра MutationObserver.
Теперь, когда у нас есть функциональная и базовая версия нашего хука, мы можем подумать об улучшении ее качества, используя его API и улучшая опыт разработчиков этого совместно используемого фрагмента кода.
Проверка ввода и разработка
Одним из важных аспектов при разработке пользовательских хуков React является проверка ввода. Мы должны иметь возможность общаться с разработчиками, когда что-то идет не так, как положено, или когда определенный вариант использования выходит за пределы допустимого.
Обычно журналы разработки помогают разработчикам понять незнакомый код для корректировки своей реализации. Точно так же мы можем улучшить описанную выше реализацию, добавив проверки времени выполнения и подробные журналы предупреждений для проверки и передачи проблем другим разработчикам:
function useMutationObservable(targetEl, cb, options = DEFAULT_OPTIONS) { const [observer, setObserver] = useState(null); useEffect(() => { // A) if (!cb || typeof cb !== "function") { console.warn( `You must provide a valid callback function, instead you've provided ${cb}` ); return; } const { debounceTime } = options; const obs = new MutationObserver(cb); setObserver(obs); }, [cb, options, setObserver]); useEffect(() => { if (!observer) return; if (!targetEl) { // B) console.warn( `You must provide a valid DOM element to observe, instead you've provided ${targetEl}` ); } const { config } = options; try { observer.observe(targetEl, config); } catch (e) { // C) console.error(e); } return () => { if (observer) { observer.disconnect(); } }; }, [observer, targetEl, options]); }
В этом примере мы проверяем, что обратный вызов передается в качестве второго аргумента. Эта проверка API во время выполнения может легко предупредить разработчика о том, что на стороне вызывающего абонента что-то не так. Мы также можем увидеть, является ли предоставленный элемент DOM недопустимым с ошибочным значением, предоставленным хуку во время выполнения или нет. Они логируются вместе, чтобы сообщить нам о проблеме для быстрого ее решения.
И, если observe выдает ошибку, мы можем ее поймать и отследить. Мы должны по возможности избегать прерывания потока выполнения JavaScript, поэтому, поймав ошибку, мы можем выбрать ее регистрацию или отчет в зависимости от среды.
Расширяемость через конфигурацию
Если мы хотим добавить больше возможностей в наш Hook, мы должны сделать это в ретро-совместимой манере, например, с возможностью выбора, которая практически не препятствует ее внедрению.
Давайте посмотрим, как мы можем дополнительно отменить предоставленную функцию обратного вызова, чтобы вызывающие абоненты могли указать интервал времени, в течение которого в триггере целевого элемента нет других изменений. Это запускает обратный вызов один раз, а не запускает такое же количество раз, сколько мутировали элемент или его дочерние элементы:
import debounce from "lodash.debounce"; const DEFAULT_OPTIONS = { config: { attributes: true, childList: true, subtree: true }, debounceTime: 0 }; function useMutationObservable(targetEl, cb, options = DEFAULT_OPTIONS) { const [observer, setObserver] = useState(null); useEffect(() => { if (!cb || typeof cb !== "function") { console.warn( `You must provide a valida callback function, instead you've provided ${cb}` ); return; } const { debounceTime } = options; const obs = new MutationObserver( debounceTime > 0 ? debounce(cb, debounceTime) : cb ); setObserver(obs); }, [cb, options, setObserver]); // ...
Это удобно, если нам нужно выполнить тяжелую операцию, такую как запуск веб-запроса, гарантируя, что он выполняется минимально возможное количество раз.
Добавим опцию debounceTime в наш пользовательский Hook. Если в MutationObservable передается значение больше 0, обратный вызов соответственно откладывается.
С простой конфигурацией, представленной в нашем Hook API, мы позволяем другим разработчикам устранять ошибки обратного вызова, что может привести к более производительной реализации, учитывая, что мы можем значительно сократить количество выполнений кода обратного вызова.
Конечно, мы всегда можем отменить обратный вызов на стороне клиента, но таким образом мы улучшаем наш API и делаем реализацию на стороне вызывающего меньше и декларативнее.
Тестирование
Тестирование — неотъемлемая часть разработки. Это помогает нам обеспечить определенный уровень качества для общих API-интерфейсов, когда они активно используются и распространяются.
Руководство по тестированию React Hooks содержит подробные сведения о тестировании, которые могут быть реализованы следуя этой документации.
Документация
Документация может повысить качество пользовательских хуков и сделать их удобными для разработчиков.
Но даже при написании простого JavaScript, может быть написана документация JSDoc для пользовательских API-интерфейсов, чтобы гарантировать, что Hook передает правильное сообщение разработчикам.
Давайте сосредоточимся на объявлении функции useMutationObservable и на том, как добавить к нему отформатированную документацию JSDoc:
/** * Эти пользовательские хуки абстрагируют использование Mutation Observer с компонентами React.. * Следят за изменениями, вносимыми в дерево DOM, и запускают собственный обратный вызов. * @param {Element} targetEl DOM элемент, подлежащий наблюдению * @param {Function} cb будет запускаться обратный вызов при изменении targetEl или любого другого * дочерний элемент (в зависимости от предоставленных опций) * @param {Object} опции * @param {Object} options.config проверка \[options\](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe) * @param {number} [options.debounceTime=0] число, которое представляет количество времени в мс * после которого вы должны отключить вызов предоставленной функции обратного вызова */ function useMutationObservable(targetEl, cb, options = DEFAULT_OPTIONS) {
Написание этого полезно не только для документации, но и для использования возможностей IntelliSense, которые автоматически заполняют использование хуков и предоставляют точечную информацию о их параметрах. Это экономит разработчикам несколько секунд при каждом использовании, потенциально добавляя несколько часов, потраченных на чтение кода и попытки понять его.
Заключение
С помощью различных типов пользовательских хуков, которые мы можем реализовать, мы видим, как они интегрируют внешние API в мир React. Мы можем легко интегрировать управление состоянием в хуки и запускать разные эффекты на основе входных данных от компонентов.
Помните, что для создания качественных хуков важно для:
Создания простых в использовании декларативных API
Повышения удобства разработки, проверяя правильность использования и регистрируя предупреждения и ошибки.
Предоставления возможностей конфигурирования, таких, как например debounceTime
Упрощения использования хуков (написав документацию JSDoc).
Вы можете ознакомится с полной реализацией пользовательских хуков React.
Автор: Daniel Caldas
Источник: blog.logrocket.com
Редакция: Команда webformyself.
Читайте нас в Telegram, VK, Яндекс.Дзен