Как создать собственный хук React для извлечения и кэширования данных
Опубликовано: 2022-03-10componentDidMount()
, но с введением хуков вы можете создать собственный хук, который будет извлекать и кэшировать данные для вас. Это то, что этот учебник будет охватывать.Если вы новичок в 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
с пустым объектом в качестве начального значения.
Подведение итогов
Что ж, я заявил, что установка данных перед установкой статуса извлечения была хорошей идеей, но с этим у нас также могут возникнуть две потенциальные проблемы:
- Наш модульный тест может завершиться ошибкой из-за того, что массив данных не будет пустым, пока мы находимся в состоянии выборки. React может на самом деле пакетно изменять состояние, но он не может этого сделать, если запускается асинхронно;
- Наше приложение перерисовывает больше, чем должно.
Давайте сделаем окончательную очистку нашего хука 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», Аденей Дэвид Абиодун
- «Функциональное программирование: чистые функции», Арне Брассер