Как создать пользовательский хук 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.

Данная публикация представляет собой перевод статьи «How To Create A Custom React Hook To Fetch And Cache Data» , подготовленной дружной командой проекта Интернет-технологии.ру

Меню