DevGang
Авторизоваться

Получение основных данных в React

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

В этой статье я покажу вам, как перейти от наивного подхода к получению данных к эффективному. Вы также научитесь использовать такие библиотеки, как react-query и SWR.

Развитие наивного подхода

Я могу гарантировать, что вы видели подобный код или сами писали его.

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

type Beer = {
  id: number,
  brand: string,
  name: string,
  yeast: string,
  malts: string,
  alcohol: string,
}

function NaiveDataFetching(){
  const [beer, setBeer] = useState<Beer | undefined>();
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(true);

  useEffect(() => {
   fetch("https://random-data-api.com/api/v2/beers")
   .then(res => res.json()).then((data: Beer) => {
    setLoading(false);
    setBeer(data)
   }).catch(error => {
    console.log(error); 
    setError("Something went wrong")})
  },[])

  if(error) return <div>{error}</div>

  if(loading) return <div>loading...</div>

  return <div>
    <p>id: {beer?.id}</p>
    <p>brand: {beer?.brand}</p>
    <p>name: {beer?.name}</p>
    <p>yeast: {beer?.yeast}</p>
    <p>malts: {beer?.malts}</p>
    <p>alcohol: {beer?.alcohol}</p>
  </div>
}

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

Если включен строгий режим, то перед первой реальной установкой React выполнит один дополнительный цикл настройки+очистки, предназначенный только для разработки. Это стресс-тест, который гарантирует, что ваша логика очистки "зеркально" отражает логику установки и останавливает или отменяет все, что делает установка. Если это вызывает проблемы, реализуйте функцию очистки.

Исправление условий гонки

В React 18 во время разработки хук useEffect вызывается дважды. Получение данных без реализации функции очистки приводит к так называемым "условиям гонки". Это происходит потому, что ответы из сети могут приходить не в том порядке, в котором отправлялись запросы, в результате чего вы увидите результаты нескольких запросов, а не только последнего.

Видно, что данные обновляются дважды, что не является желаемым поведением.

Как же решить эту проблему? Следующий способ взят из документации по React.

Решить эту проблему можно, введя переменную ignore, изначально имеющую значение false. Когда мы получаем данные, мы проверяем, является ли эта переменная истинной. Если true, то мы соответствующим образом присваиваем данные пиву. Но как изменить значение ignore на true? Для этого мы должны реализовать функцию очистки, отвечающую за установку ignore обратно в false, которая будет вызываться при размонтировании компонента. Во время первоначального монтирования данные будут игнорироваться, поскольку после размонтирования ignore будет установлен в true. В результате при повторном монтировании будут получены самые последние данные. Это происходит исключительно в процессе разработки; в производственных условиях useEffect выполняется только один раз.

useEffect(() => {
   let ignore = false;
   fetch("https://random-data-api.com/api/v2/beers")
   .then(res => res.json()).then((data: Beer) => {
    if(!ignore) {
      setLoading(false);
      setBeer(data)
    }  
   }).catch(error => {
    console.log(error); 
    setError("Something went wrong")})

    return () => {ignore = true}
  },[])

Несмотря на то, что это работает, есть небольшая проблема. Первый запрос всё равно выполняется, даже если компонент отключен, мы просто не обновляем состояние.

Внедрение контроллера отмены

Интерфейс AbortController представляет объект контроллера, позволяющий при необходимости прерывать один или несколько Web-запросов.

Это именно то, что нам нужно. Мы просто хотим прерывать нерелевантные запросы.

useEffect(() => {
  const controller = new AbortController();

   fetch("https://random-data-api.com/api/v2/beers", {signal: controller.signal})
   .then(res => res.json()).then((data: Beer) => {
      setLoading(false);
      setBeer(data)
   }).catch(error => {

    if(error instanceof DOMException){
      console.log("Request aborted", error.message); 
      return;
    }

    setError("Something went wrong")})

    return () => controller.abort();
  },[])

Примечание: При вызове abort() обещание fetch() будет отклонено с DOMException с именем AbortError. В данном случае мы хотим видеть только ошибку, связанную с выборкой данных, а не прерывание запроса.

Проблемы, связанные с данным подходом

Получение данных таким образом делает наш код тесно связанным с компонентом. А что, если нам понадобится другой компонент Beer, который будет использовать эти данные по-другому? Это также нарушает принципы SOLID, поскольку компонент должен реагировать только на отображение данных. (Раньше это можно было исправить с помощью HOC, теперь у нас есть пользовательские хуки).

Использование хука useFetchBeerData

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

function useFetchBeerData(){
  const [beer, setBeer] = useState<Beer | undefined>();
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(true);

  useEffect(() => {
   const controller = new AbortController();

   fetch("https://random-data-api.com/api/v2/beers", {signal: controller.signal})
   .then(res => res.json()).then((data: Beer) => {
      setLoading(false);
      setBeer(data)
   }).catch(error => {

    if(error instanceof DOMException){
      console.log("Request aborted", error.message); 
      return;
    }

    setError("Something went wrong")})

    return () => controller.abort();
  },[])

  return {beer, error, loading}
}

function NaiveDataFetching(){
  const {beer, error, loading} = useFetchBeerData();

  // Rest of your component logic
}

Это замечательно. Теперь мы можем повторно использовать логику выборки в любом компоненте, не копируя и не вставляя код. Однако есть одна проблема - он слишком специфичен. Что если мы хотим получить элемент Beer, который не соответствует нашему интерфейсу Beer, имеет другой URL или по другим причинам?

Не беспокойтесь. Я позабочусь о вас. Мы можем сделать наш хук более универсальным.

export function useFetch<T>(
 fetcher: (signal: AbortSignal) => Promise<T>,
 rerun?: any[]
): {
 data?: T;
 loading: boolean;
 error: unknown;
} {
 const [data, setData] = useState<T>();
 const [loading, setLoading] = useState(true);
 const [error, setError] = useState();

 const deps = rerun ? [...rerun] : [];

 useEffect(() => {
  const controller = new AbortController();

  fetcher(controller.signal)
   .then(d => {
    setLoading(false);
    setData(d);
   })
   .catch(error => {
    setError(error);
   });

  return () => controller.abort();
 }, [...deps]);

 return {
  data,
  loading,
  error,
 };
}

Fetcher - это просто функция, которую мы передаем нашему хуку. Ее назначение - отделить логику выборки от самого хука, что позволяет нам реализовать различные варианты. Например:

async function fetchBeer(signal: AbortSignal){
  const { data } = await axios.get<Beer>("https://random-data-api.com/api/v2/beers", {signal});

  return data;
 }

Или же следующее:

 async function fetchBeer(signal: AbortSignal){
  const response = await fetch("https://random-data-api.com/api/v2/beers", {signal});
  const data = await response.json();

  return data as Beer;
 }

И просто переключайте их по мере необходимости.

function BetterDataFetching(){
  const {data, error, loading} = useFetch<Beer>(fetchBeer)

  // This is a viable way to check for errors. Note: For axios replace DOMException with CanceledError.
  // Checking for errors thrown by Aborting a request is only specific to my example so the code can render. 

  if(error && !(error instanceof DOMException)) return <div>{(error as Error).message}</div>

  // Rest of your component logic

Что делает параметр rerun? По сути, он предоставляет нам возможность повторной выборки данных при наличии зависимостей. Например, если в строке запроса есть такое свойство, как budget, мы хотим, чтобы наши данные обновлялись при каждом его изменении.

Этот код намного лучше того, с которого мы начали, и он прекрасно подойдет, если вам не нужны все преимущества использования библиотеки. Для простых проектов это идеальный вариант. Он оказался ценным в проекте, над которым я работал во время летней стажировки в Roweb.

React Query

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

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

Давайте посмотрим, как можно реализовать react-query в нашем примере. Для начала установите его в свой проект npm i @tanstack/react-query. Для эффективного использования React Query необходимо обернуть код приложения провайдером QueryClientProvider. Если пропустить этот шаг, React Query работать не будет.

const client = new QueryClient();

function App() {
  return (
      <QueryClientProvider client={client}>
        <ReactQueryFetching/>
      </QueryClientProvider>
  );
}

После того как вы обернули свое приложение с помощью QueryClientProvider, вы можете легко получать данные с помощью React Query.

function ReactQueryFetching(){
  const {data: beer, error, isLoading} = useQuery<Beer>({ queryKey: ['beer'], queryFn: fetchBeer })

  if(error instanceof Error) return <div>{error.message}</div>

  if(isLoading) return <div>loading...</div>

  return <div>
    <p>id: {beer?.id}</p>
    <p>brand: {beer?.brand}</p>
    <p>name: {beer?.name}</p>
    <p>yeast: {beer?.yeast}</p>
    <p>malts: {beer?.malts}</p>
    <p>alcohol: {beer?.alcohol}</p>
  </div>
}

Хук useQuery обрабатывает различные аспекты получения и управления данными, включая кэширование, дедупликацию запросов, ревалидацию и т.д.

Параметр queryKey принимает ключ, который может представлять собой массив строк, чисел и объектов. Ключ используется для кэширования данных, что может быть полезно во многих случаях. Ключи запросов хешируются автоматически! Это означает, что независимо от порядка следования ключей в объектах, они считаются одинаковыми.

useQuery({ queryKey: ['todos', { status, page }], ... })

// they are the same

useQuery({ queryKey: ['todos', { page, status }], ...})

В queryFn задается логика получения данных для запроса, а React Query использует эту функцию для управления данными и обеспечения кэширования, повторной проверки и других возможностей. Это та же концепция, которую мы использовали в пользовательском хуке.

Обработка и выброс ошибок

Функцией запроса может быть любая функция, возвращающая обещание. Возвращаемое обещание должно либо разрешить данные, либо выдать ошибку. Любая ошибка, возникающая в функции запроса, сохраняется в состоянии error запроса.

async function fetchBeer(){
 const response = await fetch("https://random-data-api.com/api/v2/beers");

 if (!response.ok) {
  // this will be persisted in our error state
  throw new Error('Network response was not ok') 
}

 const data = await response.json();

 return data as Beer;
}

Мутации данных

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

// Mutation function

async function addBeer(newBeer: FormData){
  await fetch("https://example.beer-api.com", {
    method: "POST",
    body: newBeer
  });
}

// Inside component

 const queryClient = useQueryClient()

 const mutation = useMutation({
    mutationFn: addBeer,
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['beers'] })
  })

 const onSubmit = (event) => {
    event.preventDefault()
    mutation.mutate(new FormData(event.target))
  }

 return <form onSubmit={onSubmit}>...</form>

Это все, что вам нужно для начала работы. Если вы хотите узнать больше о react-query, посетите их сайт.

SWR

SWR (Stale-While-Revalidate)(("задержка при перепроверке")) - популярная JavaScript-библиотека, используемая для получения и кэширования данных в приложениях на стороне клиента. Она часто используется в React и других современных фронтенд-фреймворках для упрощения управления удаленными данными.

Внутри каталога проекта React выполните следующие действия: npm i swr.

Затем можно импортировать useSWR и начать использовать его внутри любых функциональных компонентов:

import useSWR from 'swr'

async function fetchBeer(){
  const response = await fetch("https://random-data-api.com/api/v2/beers");
  const data = await response.json();

  return data as Beer;
 }

function Beer() {
  const { data, error, isLoading } = useSWR('beer', fetchBeer)

  if (error) return <div>failed to load</div>;

  if (isLoading) return <div>loading...</div>;

  return <div>Brand - {beer.brand}!</div>
}

Первый параметр принимает строку, представляющую собой ключ, как и в react-query, используемый для кэширования данных. Второй параметр принимает функцию fetcher, в которой задается логика получения данных. Третий параметр является необязательным, он принимает объект опций.

const { data, error, isLoading, isValidating, mutate } = useSWR(key, fetcher, options)

Примечание: По умолчанию в качестве аргумента в fetcher передается key. Поэтому следующие три выражения эквивалентны:

useSWR('beer', () => fetcher('beer'))
useSWR('beer', url => fetcher(url))
useSWR('beer', fetcher)

Замечательной особенностью этой библиотеки является автоматическая перепроверка данных при перефокусировке страницы или переключении между вкладками.

Обработка и выброс ошибок

Если внутри fetcher возникла ошибка, то она будет возвращена хуком как error.

async function fetchBeer(){
  const response = await fetch("https://random-data-api.com/api/v2/beers");

  if (!response.ok) {
    // this will be persisted in our error state
    throw new Error('Network response was not ok') 
  }

  const data = await response.json();

  return data as Beer;
 }

Мутации данных

В сценарии, описанном ранее в react-query, где мы имеем дело с массивом пива, можно легко обновить данные, вызвав функцию mutate с новыми данными. Такой простой подход позволяет беспрепятственно выполнять мутацию данных и обновление пользовательского интерфейса.

const { data, mutate } = useSWR('beers', fetcher)

const onSubmit = (event) => {
    event.preventDefault()

    const newBeer = { ... }

    mutate([...data, newBeer]);
  }

 return <form onSubmit={onSubmit}>...</form>

В данном контексте нет необходимости в ключе, как в React Query, поскольку функция mutate напрямую связана с компонентом.

Перепроверка

Если вызвать mutate(key) или просто mutate() без каких-либо данных, то это вызовет перепроверку ресурса.

// tell all SWRs with this key to revalidate

<button onClick={() => mutate('beer')}>Revalidate</button>

Это все, что нужно для начала работы с SWR. Если вы хотите узнать больше, посетите их сайт.

Конечные примечания

Обе эти библиотеки позволяют создавать бесконечную прокрутку или пагинацию. Поскольку это выходит за рамки данного руководства, я не буду это обсуждать.

Эта статья служит начальным руководством по получению данных в React. Если вы хотите углубиться в эту тему, я рекомендую обратиться к документации соответствующих библиотек, поскольку в них содержится исчерпывающая информация. В данном случае мы стремились предоставить вам основную информацию, которая поможет вам быстро начать работу.

Если у вас возникли вопросы по этой статье или она оказалась полезной, пожалуйста, оставьте свой отзыв и следите за новыми материалами. Мы очень ценим ваши отзывы и участие!

Источник:

#JavaScript #React #TypeScript
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу