Учебник о том, как использовать лучшее из React Query и Next.js для оптимизации вызовов API и визуализации данных в представлении

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

Выборка на стороне сервера Next.js

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

Next.js предлагает функцию уровня страницы под названием getServerSideProps, которая позволяет нам выполнять запрос на выборку NodeJS и передавать его в качестве реквизита на страницу. Это дает нам преимущество наличия готовых данных на стороне клиента.

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

const fetchCompanies = async () => {
  const res = await fetch("https://fakerapi.it/api/v1/companies");

  if (!res.ok) {
    throw new Error("Something went wrong");
  }

  const { data = [] } = await res.json();

  return data;
};

export default function Home({ data }) {
  return (
    <div>
      <h1>Companies</h1>
      <ol>
        {data && data.map(({ id, name, email, vat, phone, website }) => (
          <li key={id}>
            <h2>{name}</h2>
            <p>{email}</p>
            <p>{vat}</p>
            <p>{phone}</p>
            <p>{website}</p>
          </li>
        ))}
      </ol>
    </div>
  );
};

export const getServerSideProps = async () => {
  const data = await fetchCompanies()
  return {
    props: {
      data
    },
  };
};

Вид страницы

Выглядит отлично, правда? Список отображается почти мгновенно, и нам даже не нужно показывать состояние загрузки (пока).

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

Реагировать на запрос на помощь

React Query — отличная библиотека выборки данных, которая позволяет нам использовать первоначальные выборки на стороне сервера, повторные попытки, кэширование, тайм-ауты запросов и многое другое!

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

Для начала нам нужно обернуть наши страницы в провайдерах React Query внутри _app.js.

import { QueryClient, QueryClientProvider, Hydrate } from "@tanstack/react-query";
// Create a client
const queryClient = new QueryClient();
export default function MyApp({ Component, pageProps }) {
  return (
    <QueryClientProvider client={queryClient}>
      <Hydrate state={pageProps.dehydratedState}>
        <Component {...pageProps} />
      </Hydrate>
    </QueryClientProvider>
  );
}

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

Пересмотр нашего первоначального подхода

import { QueryClient, dehydrate } from "@tanstack/react-query";

const fetchCompanies = async () => {
  const res = await fetch("https://fakerapi.it/api/v1/companies");

  if (!res.ok) {
    throw new Error("Something went wrong");
  }

  const { data = [] } = await res.json();

  return data;
};

export default function Home() {
  const { data, error, isLoading } = useQuery({ queryKey: 'companies', queryFn: fetchCompanies, staleTime: 60_000 }); // stale after 1 min
  if (isLoading){
   return <h1>Loading...</h1>
  }
  if (error){
   return <h1>{error.message}</h1>
  }
  return (
    <div>
      <h1>Companies</h1>
      <ol>
        {data && data.map(({ id, name, email, vat, phone, website }) => (
          <li key={id}>
            <h2>{name}</h2>
            <p>{email}</p>
            <p>{vat}</p>
            <p>{phone}</p>
            <p>{website}</p>
          </li>
        ))}
      </ol>
    </div>
  );
};

export const getServerSideProps = async () => {
  const queryClient = new QueryClient()
  await queryClient.prefetchQuery(["companies"], fetchCompanies);

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
  };
};

Что изменилось?

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

Наше преимущество в том, что наши данные будут обновляться на стороне клиента, когда они устареют. Данные могут перейти в состояние из-за истечения срока действия нашего кеша, указания тайм-аута для свежих данных или выполнения другого запроса (например, создания новой записи данных с запросом POST), что вызывает аннулирование текущих данных.

Перепроверив вкладку сети, мы видим, что выборка на стороне клиента запускается, как только данные считаются устаревшими. В результате React Query будет пытаться восстановить данные несколько раз, если первоначальный запрос завершится ошибкой.

Вывод (почти)

Вот и все! Мы сделали наше приложение производительным и реагирующим на сбои запросов. Кроме того, React Query можно настроить для дальнейшей оптимизации состояния и запросов приложения в зависимости от типа разрабатываемого приложения.

Сказав это, есть еще одна вещь, которую мы можем сделать…

Рефакторинг

Давайте очистим этот код и превратим его во что-то более приятное.

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

const QUERY_KEYS = {
  COMPANIES: "companies",
}
const queryFunctions = {
  [QUERY_KEYS.COMPANIES]: fetchCompanies,
};

После этого мы можем взять наш хук useQuery и создать с ним HOC (компонент более высокого порядка).

export const withQuery = (Component, key) => {
  return (props) => {
    const queryResponse = useQuery({ queryKey: [key], queryFn: queryFunctions[key], staleTime: 50_000 });
    return <Component {...{...props, ...queryResponse}} />;
  };
};jsx

Этот HOC будет обертывать наш компонент страницы и распространять реквизиты ответа на запрос (вместе с реквизитами страницы) для нас, аналогично тому, как мы использовали данные, которые мы получили непосредственно от getServerSideProps в нашей первой итерации. Это дает нам преимущество простого интерфейса и абстрагирования деталей реализации.

Наш последний компонент страницы будет выглядеть так:

const Home = ({ data, error, isLoading }) => {

  if (isLoading) {
    return <h1>Loading...</h1>;
  }

  if (error) {
    return <h1>{error.message}</h1>;
  }

  return (
    <div>
      <ReactQueryDevtools initialIsOpen={false} />
      <h1>Companies</h1>
      <ol>
        {data && data.map(({ id, name, email, vat, phone, website }) => (
          <li key={id}>
            <h2>{name}</h2>
            <p>{email}</p>
            <p>{vat}</p>
            <p>{phone}</p>
            <p>{website}</p>
          </li>
        ))}
      </ol>
    </div>
  );
};

export default withQuery(Home, QUERY_KEYS.COMPANIES);

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

Я надеюсь, что это дало вам некоторое представление о том, как повысить устойчивость и производительность ваших запросов API внутри Next.js.

Удачного получения!