Как создать собственный хук React для извлечения и кэширования данных

Опубликовано: 2022-03-10
Краткое резюме ↬ Существует высокая вероятность того, что многим компонентам вашего приложения React придется выполнять вызовы API для получения данных, которые будут отображаться для ваших пользователей. Это уже можно сделать с помощью метода жизненного цикла componentDidMount() , но с введением хуков вы можете создать собственный хук, который будет извлекать и кэшировать данные для вас. Это то, что этот учебник будет охватывать.

Если вы новичок в React Hooks, вы можете начать с изучения официальной документации, чтобы разобраться в этом. После этого я бы порекомендовал прочитать «Начало работы с React Hooks API» Шедрака Акинтайо. Чтобы убедиться, что вы следите за всем, есть также статья, написанная Adeneye David Abiodun, в которой рассказывается о передовых методах работы с React Hooks, которые, я уверен, окажутся вам полезными.

На протяжении всей этой статьи мы будем использовать Hacker News Search API для создания пользовательского хука, который мы можем использовать для получения данных. В то время как это руководство будет охватывать API поиска новостей Hacker, у нас будет работа хука таким образом, что он будет возвращать ответ по любой действительной ссылке API, которую мы ему передаем.

Лучшие практики реагирования

React — это фантастическая библиотека JavaScript для создания многофункциональных пользовательских интерфейсов. Он обеспечивает отличную абстракцию компонентов для организации ваших интерфейсов в хорошо функционирующий код, и вы можете использовать его практически для всего. Прочтите соответствующую статью о React →

Получение данных в компоненте React

До перехватчиков React было принято извлекать исходные данные в методе жизненного цикла componentDidMount() , а данные, основанные на изменениях свойства или состояния, — в методе жизненного цикла componentDidUpdate() .

Вот как это работает:

 componentDidMount() { const fetchData = async () => { const response = await fetch( `https://hn.algolia.com/api/v1/search?query=JavaScript` ); const data = await response.json(); this.setState({ data }); }; fetchData(); } componentDidUpdate(previousProps, previousState) { if (previousState.query !== this.state.query) { const fetchData = async () => { const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${this.state.query}` ); const data = await response.json(); this.setState({ data }); }; fetchData(); } }

Метод жизненного цикла componentDidMount вызывается, как только компонент монтируется, и когда это будет сделано, мы сделали запрос на поиск «JavaScript» через Hacker News API и обновили состояние на основе ответа.

С другой стороны, метод жизненного цикла componentDidUpdate вызывается при изменении компонента. Мы сравнили предыдущий запрос в состоянии с текущим запросом, чтобы предотвратить вызов метода каждый раз, когда мы устанавливаем «данные» в состоянии. Одна вещь, которую мы получаем от использования хуков, — это более чистое объединение обоих методов жизненного цикла — это означает, что нам не нужно будет иметь два метода жизненного цикла, когда компонент монтируется и когда он обновляется.

Еще после прыжка! Продолжить чтение ниже ↓

Получение данных с помощью useEffect

useEffect вызывается сразу после монтирования компонента. Если нам нужно, чтобы хук перезапустился на основе некоторых изменений свойства или состояния, нам нужно передать их в массив зависимостей (который является вторым аргументом хука useEffect ).

Давайте рассмотрим, как получать данные с помощью хуков:

 import { useState, useEffect } from 'react'; const [status, setStatus] = useState('idle'); const [query, setQuery] = useState(''); const [data, setData] = useState([]); useEffect(() => { if (!query) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${query}` ); const data = await response.json(); setData(data.hits); setStatus('fetched'); }; fetchData(); }, [query]);

В приведенном выше примере мы передали query как зависимость нашему useEffect . Делая это, мы useEffect отслеживать изменения запроса. Если предыдущее значение query не совпадает с текущим значением, useEffect вызывается снова.

С учетом сказанного, мы также устанавливаем несколько status компонента по мере необходимости, так как это лучше передает некоторое сообщение на экран на основе некоторых конечных состояний status . В состоянии ожидания мы могли бы сообщить пользователям, что они могут использовать окно поиска, чтобы начать работу. В состоянии выборки мы могли бы показать счетчик . И в полученном состоянии мы будем отображать данные.

Важно установить данные до того, как вы попытаетесь установить статус fetched , чтобы вы могли предотвратить мерцание, возникающее в результате того, что данные пусты, когда вы устанавливаете статус fetched .

Создание пользовательского хука

«Пользовательский хук — это функция JavaScript, имя которой начинается с «использовать» и которая может вызывать другие хуки».

— Реагировать Документы

Это действительно так, и вместе с функцией JavaScript он позволяет вам повторно использовать некоторый фрагмент кода в нескольких частях вашего приложения.

Определение из React Docs выдало это, но давайте посмотрим, как это работает на практике с пользовательским хуком счетчика:

 const useCounter = (initialState = 0) => { const [count, setCount] = useState(initialState); const add = () => setCount(count + 1); const subtract = () => setCount(count - 1); return { count, add, subtract }; };

Здесь у нас есть обычная функция, в которой мы принимаем необязательный аргумент, устанавливаем значение для нашего состояния, а также добавляем методы add и subtract , которые можно использовать для его обновления.

Везде в нашем приложении, где нам нужен счетчик, мы можем вызывать useCounter как обычную функцию и передавать initialState , чтобы мы знали, с чего начать подсчет. Когда у нас нет начального состояния, по умолчанию используется значение 0.

Вот как это работает на практике:

 import { useCounter } from './customHookPath'; const { count, add, subtract } = useCounter(100); eventHandler(() => { add(); // or subtract(); });

Что мы сделали здесь, так это импортировали наш пользовательский хук из файла, в котором мы его объявили, чтобы мы могли использовать его в нашем приложении. Мы устанавливаем его начальное состояние равным 100, поэтому всякий раз, когда мы вызываем add() , он увеличивает count на 1, а всякий раз, когда мы вызываем subtract() , он уменьшает count на 1.

Создание useFetch

Теперь, когда мы узнали, как создать простой пользовательский хук, давайте извлечем нашу логику для извлечения данных в пользовательский хук.

 const useFetch = (query) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!query) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${query}` ); const data = await response.json(); setData(data.hits); setStatus('fetched'); }; fetchData(); }, [query]); return { status, data }; };

Это почти то же самое, что мы делали выше, за исключением того, что это функция, которая принимает query и возвращает status и data . И это хук useFetch , который мы могли бы использовать в нескольких компонентах нашего приложения React.

Это работает, но проблема с этой реализацией сейчас в том, что она специфична для Hacker News, поэтому мы можем просто назвать ее useHackerNews . Что мы собираемся сделать, так это создать хук useFetch , который можно использовать для вызова любого URL-адреса. Давайте изменим его, чтобы он вместо этого принимал URL-адрес!

 const useFetch = (url) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch(url); const data = await response.json(); setData(data); setStatus('fetched'); }; fetchData(); }, [url]); return { status, data }; };

Теперь наш хук useFetch является универсальным, и мы можем использовать его по своему усмотрению в наших различных компонентах.

Вот один из способов его употребления:

 const [query, setQuery] = useState(''); const url = query && `https://hn.algolia.com/api/v1/search?query=${query}`; const { status, data } = useFetch(url);

В этом случае, если значение query truthy , мы продолжаем устанавливать URL-адрес, а если это не так, мы можем передать неопределенное значение, поскольку оно будет обработано в нашем хуке. Эффект попытается запуститься один раз, несмотря ни на что.

Запоминание извлеченных данных

Мемоизация — это метод, который мы будем использовать, чтобы убедиться, что мы не попали в конечную точку hackernews , если мы сделали какой-то запрос на ее получение на каком-то начальном этапе. Хранение результатов дорогостоящих вызовов выборки сэкономит пользователям некоторое время загрузки и, следовательно, повысит общую производительность.

Примечание . Для получения дополнительной информации вы можете ознакомиться с объяснением Википедии о мемоизации.

Давайте посмотрим, как мы могли бы это сделать!

 const cache = {}; const useFetch = (url) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); if (cache[url]) { const data = cache[url]; setData(data); setStatus('fetched'); } else { const response = await fetch(url); const data = await response.json(); cache[url] = data; // set response in cache; setData(data); setStatus('fetched'); } }; fetchData(); }, [url]); return { status, data }; };

Здесь мы сопоставляем URL-адреса с их данными. Итак, если мы делаем запрос на получение некоторых существующих данных, мы устанавливаем данные из нашего локального кеша, в противном случае мы делаем запрос и устанавливаем результат в кеше. Это гарантирует, что мы не будем вызывать API, когда у нас есть данные, доступные нам локально. Мы также заметим, что мы отключаем эффект, если URL-адрес является falsy , поэтому он гарантирует, что мы не перейдем к выборке данных, которые не существуют. Мы не можем сделать это до хука useEffect , так как это противоречит одному из правил хуков, которое заключается в том, чтобы всегда вызывать хуки на верхнем уровне.

Объявление cache в другой области видимости работает, но наш хук идет вразрез с принципом чистой функции. Кроме того, мы также хотим убедиться, что React помогает навести порядок, когда мы больше не хотим использовать компонент. Мы useRef , чтобы помочь нам в достижении этого.

Запоминание данных с помощью useRef

« useRef подобен блоку, который может содержать изменяемое значение в своем .current property ».

— Реагировать Документы

С помощью useRef мы можем легко устанавливать и извлекать изменяемые значения, и его значение сохраняется на протяжении всего жизненного цикла компонента.

Давайте заменим нашу реализацию кеша на магию useRef !

 const useFetch = (url) => { const cache = useRef({}); const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); if (cache.current[url]) { const data = cache.current[url]; setData(data); setStatus('fetched'); } else { const response = await fetch(url); const data = await response.json(); cache.current[url] = data; // set response in cache; setData(data); setStatus('fetched'); } }; fetchData(); }, [url]); return { status, data }; };

Здесь наш кеш теперь находится в нашем useFetch с пустым объектом в качестве начального значения.

Подведение итогов

Что ж, я заявил, что установка данных перед установкой статуса извлечения была хорошей идеей, но с этим у нас также могут возникнуть две потенциальные проблемы:

  1. Наш модульный тест может завершиться ошибкой из-за того, что массив данных не будет пустым, пока мы находимся в состоянии выборки. React может на самом деле пакетно изменять состояние, но он не может этого сделать, если запускается асинхронно;
  2. Наше приложение перерисовывает больше, чем должно.

Давайте сделаем окончательную очистку нашего хука useFetch . Мы собираемся начать с переключения наших useState на useReducer . Давайте посмотрим, как это работает!

 const initialState = { status: 'idle', error: null, data: [], }; const [state, dispatch] = useReducer((state, action) => { switch (action.type) { case 'FETCHING': return { ...initialState, status: 'fetching' }; case 'FETCHED': return { ...initialState, status: 'fetched', data: action.payload }; case 'FETCH_ERROR': return { ...initialState, status: 'error', error: action.payload }; default: return state; } }, initialState);

Здесь мы добавили начальное состояние, которое является начальным значением, которое мы передали каждому из наших отдельных useState s. В нашем useReducer мы проверяем, какой тип действия мы хотим выполнить, и устанавливаем соответствующие значения для состояния на основе этого.

Это решает две проблемы, которые мы обсуждали ранее, поскольку теперь мы можем одновременно устанавливать статус и данные, чтобы помочь предотвратить невозможные состояния и ненужные повторные рендеринги.

Осталось еще одно: очистить наш побочный эффект. Fetch реализует Promise API в том смысле, что его можно разрешить или отклонить. Если наш хук попытается выполнить обновление, когда компонент размонтирован из-за того, что какое-то Promise только что было разрешено, React вернет Can't perform a React state update on an unmounted component.

Давайте посмотрим, как мы можем исправить это с помощью очистки useEffect !

 useEffect(() => { let cancelRequest = false; if (!url) return; const fetchData = async () => { dispatch({ type: 'FETCHING' }); if (cache.current[url]) { const data = cache.current[url]; dispatch({ type: 'FETCHED', payload: data }); } else { try { const response = await fetch(url); const data = await response.json(); cache.current[url] = data; if (cancelRequest) return; dispatch({ type: 'FETCHED', payload: data }); } catch (error) { if (cancelRequest) return; dispatch({ type: 'FETCH_ERROR', payload: error.message }); } } }; fetchData(); return function cleanup() { cancelRequest = true; }; }, [url]);

Здесь мы устанавливаем для cancelRequest значение true после того, как определили его внутри эффекта. Итак, прежде чем мы попытаемся внести изменения в состояние, мы сначала проверяем, был ли компонент размонтирован. Если он был размонтирован, мы пропускаем обновление состояния, а если он не был размонтирован, мы обновляем состояние. Это устранит ошибку обновления состояния React , а также предотвратит состояние гонки в наших компонентах.

Заключение

Мы рассмотрели несколько концепций хуков, помогающих извлекать и кэшировать данные в наших компонентах. Мы также провели очистку хука useEffect , который помогает предотвратить большое количество проблем в нашем приложении.

Если у вас есть какие-либо вопросы, пожалуйста, не стесняйтесь оставлять их в разделе комментариев ниже!

  • См. репозиторий для этой статьи →

использованная литература

  • «Представляем хуки», React Docs
  • «Начало работы с API React Hooks», Шедрак Акинтайо.
  • «Лучшие практики работы с React Hooks», Аденей Дэвид Абиодун
  • «Функциональное программирование: чистые функции», Арне Брассер