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

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

Если вы новичок в React Hooks, то можете легко начать с официальной документации.

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

Получение данных в компоненте 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 отслеживать изменения запроса. Если предыдущее значение не совпадает с текущим, useEffect вызывается снова.

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

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

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

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

  • Документация React

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

Определение из официальной документации React описывает пользовательский хук, но давайте посмотрим, как данное работает на практике при помощи пользовательского хука счетчика.

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(); // или 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 };};

Данное почти то же самое, что мы делали раньше, за исключением того, что данное функцию, которая принимает запрос и возвращает данные статуса. И данное хук 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-адрес, а если нет, можем передать undefined, так как он будет обработан в нашем хуке. В любом случае попытка действия будет выполнена один раз.

Мемоизация извлеченных данных

Мемоизация — данное способ, который можно использовать, чтобы убедиться, что мы не достигли конечной точки 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; // устанавливаем ответ в кэше;                setData(data);                setStatus('fetched');            }        };        fetchData();    }, [url]);    return { status, data };};

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

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

Мемоизация данных при помощи useRef

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

  • Документация React

При помощи 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; // устанавливаем ответ в кэше;                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. В 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.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *