Главная / Статьи

Аутентификация в React, загрузка данных и обработка ошибок: Axios, React Router, Redux, TanStack Query

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

Издатель: REACTAPP.TOP15 мин чтения3360 слов

Источники: Stop Polluting Your Components: The React ‘Middleware’ Guide (Axios, Routers, and State)

На стороне сервера никто не проверяет авторизацию в каждом обработчике маршрутов и не вставляет код логирования в каждый контроллер; эта работа выполняется в промежуточном слое, который подключается один раз и применяется ко всем запросам. В кодовых базах React часто отсутствует такая дисциплина. Компоненты вынуждены сами обрабатывать вставку токенов, перенаправления 401, логирование ошибок и загрузку данных, что приводит к появлению шаблонного кода, неоднородного поведения и ошибок, которые проявляются только тогда, когда пользователь нажимает на элементы в определенном порядке. У React-приложения есть собственный жизненный цикл запроса, при котором вызовы API, переходы между маршрутами и обновления состояния проходят через слои, к которым можно подключиться. В этом руководстве рассматриваются десять распространенных ошибок, связанных с интерцепторами Axios, загрузчиками React Router, библиотеками состояния, границами обработки ошибок и промежуточными слоями Next.js, а также указывается, где должна находиться каждая из этих функций.

Вопросы транспортировки: авторизация и интерцепторы Axios

Ошибка 1: обработка ответов 401 в каждом компоненте

Обработка аутентификации — наиболее частая причина избыточности кода в компонентах. Каждая функция запроса содержит собственный блок catch, который проверяет наличие кода 401, очищает хранилище данных и перенаправляет пользователя:

// ❌ Bad: Repeated in every component
async function fetchDashboard() {
  try {
    const res = await axios.get('/api/dashboard');
    return res.data;
  } catch (error) {
    if (error.response?.status === 401) {
      localStorage.removeItem('token');
      window.location.href = '/login';
    }
    throw error;
  }
}
async function fetchProfile() {
  try {
    const res = await axios.get('/api/profile');
    return res.data;
  } catch (error) {
    if (error.response?.status === 401) {
      localStorage.removeItem('token');
      window.location.href = '/login';
    }
    throw error;
  }
}

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

Решение заключается в использовании интерцептора ответов Axios на общем экземпляре клиента, который обрабатывает каждый ответ раньше, чем это сделают компоненты. Приведённая ниже версия также добавляет токен при выходе и при ошибке 401 пытается его обновить один раз (с использованием флага _retry для предотвращения бесконечного цикла), повторяет первоначальный запрос с новым токеном и переходит к очистке хранилища и перенаправлению только в случае неудачи самого обновления:

// api/client.js
import axios from 'axios';
const api = axios.create({
  baseURL: process.env.REACT_APP_API_URL,
});
// Request interceptor: attach token
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});
// Response interceptor: handle global errors
api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        // Attempt token refresh
        const refreshToken = localStorage.getItem('refreshToken');
        const { data } = await axios.post('/auth/refresh', { refreshToken });

        localStorage.setItem('token', data.accessToken);
        originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;

        return api(originalRequest);
      } catch (refreshError) {
        // Refresh failed, redirect to login
        localStorage.removeItem('token');
        localStorage.removeItem('refreshToken');
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }
    return Promise.reject(error);
  }
);
export default api;

Теперь компоненты просто импортируют api и вызывают его, не учитывая обработку ошибок 401; теперь единственным местом, где решается, как восстановить аутентификацию, является интерцептор. Существуют три важных замечания. process.env.REACT_APP_API_URL — это стандарт Create React App; в проекте Vite следует использовать import.meta.env.VITE_API_URL. Если несколько запросов сбиваются одновременно, каждый из них запускает собственную попытку обновления, что может сделать недействительными токены обновления с циклическим сроком действия; решение этой проблемы описано в статье о том, почему одновременные ошибки 401 выводят пользователей. Кроме того, хранение токенов в localStorage делает их уязвимыми к любым уязвимостям типа XSS, поэтому стоит рассмотреть использование httpOnly-куки.

Ошибка 2: отсутствие возврата значения из интерцептора

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

// ❌ Bad: Broken chain
api.interceptors.response.use((response) => {
  console.log('Response received:', response.status);
  // Forgot to return response!
});
api.interceptors.request.use((config) => {
  config.headers['X-Request-ID'] = crypto.randomUUID();
  // Forgot to return config!
});

Этот эффект часто описывают как зависание запроса, но в текущих версиях Axios сбой обычно бывает более прямым и не менее запутанным. Интерцептор ответа, возвращающий ничего, решает обещание вызывающего кода значением undefined, поэтому строка const { data } = await api.get('/user') вызывает ошибку деконструкции, совершенно не связанную с её причиной. Интерцептор запроса, возвращающий ничего, оставляет Axios без конфигурации, в результате чего запрос терпит неудачу ещё до отправки. В обоих случаях ошибка проявляется в месте, отличном от того, где она была вызвана.

Всегда возвращайте то, что получили, и явно отклоняйте запросы в обработчиках ошибок, чтобы блок catch вызывающего кода всё равно выполнялся:

// ✅ Good: Complete chain
api.interceptors.request.use(
  (config) => {
    config.headers['X-Request-ID'] = crypto.randomUUID();
    return config; // Critical
  },
  (error) => Promise.reject(error)
);
api.interceptors.response.use(
  (response) => {
    // Log without breaking the chain
    console.log(`[${response.config.url}] ${response.status}`);
    return response; // Critical
  },
  (error) => {
    // Always reject so the component catch block fires
    return Promise.reject(error);
  }
);

Аналогия с Express сохраняется: игнорирование метода next() приводит к замедлению обработки запроса в Express, а игнорирование метода return нарушает цепочку интерцепторов.

Ошибка 3: ручное добавление заголовков аутентификации

Копирование заголовка Authorization в каждый запрос приводит к тому, что общая проблема рассматривается как локальная. Рано или поздно в каком-то запросе этот заголовок будет пропущен, его формат изменится или будет отправлено значение Bearer null. Приведённый ниже фрагмент также показывает, насколько легко допустить ошибку: в запросе типа post объект заголовка передаётся как тело запроса, а данные — как параметры конфигурации, поскольку Axios ожидает синтаксис post(url, data, config):

// ❌ Bad: Manual headers everywhere
api.get('/user', { headers: { Authorization: `Bearer ${token}` } });
api.post('/order', { headers: { Authorization: `Bearer ${token}` } }, payload);
api.get('/history', { headers: { Authorization: `Bearer ${token}` } });

Интерцептор запросов устраняет всю категорию ошибок путем добавления заголовка в одном месте, но только при наличии токена:

// ✅ Good: Global token injection
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('access_token');

  if (token && config.headers) {
    config.headers.Authorization = `Bearer ${token}`;
  }

  return config;
});

Если вы используете TanStack Query, его кэши также предоставляют глобальные хуки для обработки побочных эффектов. Функция QueryCache onError может перенаправлять запросы с кодом 401, а функция MutationCache onError может отображать уведомление при любой неудачной мутации:

// ✅ Good: QueryClient global handlers
const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error) => {
      if (error.response?.status === 401) {
        window.location.href = '/login';
      }
    }
  }),
  mutationCache: new MutationCache({
    onError: (error) => {
      toast.error(error.message);
    }
  })
});

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

Загрузка данных: загрузчики React Router и кэши TanStack Query

Ошибка 4: вызов запроса в useEffect вместо использования загрузчика маршрута

Типичная схема включает монтирование компонента, выполнение эффекта, отображение индикатора загрузки, получение данных и их отрисовку:

// ❌ Bad: Component-level fetching
function Dashboard() {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
    api.get('/api/dashboard')
      .then(res => setData(res.data))
      .finally(() => setIsLoading(false));
  }, []);
  if (isLoading) return <Spinner />;
  return <DashboardContent data={data} />;
}

Это приводит к созданию «водопадов запросов»: роутер сначала отрисовывает компонент, только затем эффект отправляет запрос, и только после его возврата обновляется состояние. Переход туда-обратно каждый раз приводит к повторному загрузу данных. В более старых версиях React также появлялись предупреждения о необходимости не изменять состояние после демонтирования компонента; в React 18 это предупреждение было устранено, но соперничество между медленным и более быстрым ответом по-прежнему существует.

API для работы с данными в React Router (доступные с createBrowserRouter начиная с версии v6.4) позволяют маршруту указать loader, который загружает данные до отрисовки элемента маршрута, а также errorElement, который отображается, если loader выбрасывает исключение:

// ✅ Good: Route-level data loading
// router.jsx
import { createBrowserRouter } from 'react-router-dom';
import { Dashboard, dashboardLoader } from './Dashboard';
const router = createBrowserRouter([
  {
    path: '/dashboard',
    element: <Dashboard />,
    loader: dashboardLoader, // Runs before render
    errorElement: <DashboardError />
  }
]);
// Dashboard.jsx
import { useLoaderData } from 'react-router-dom';
export async function dashboardLoader() {
  const response = await api.get('/api/dashboard');
  if (response.status !== 200) {
    throw new Response('Failed to load dashboard', { status: response.status });
  }
  return response.data;
}
export function Dashboard() {
  const data = useLoaderData(); // Already available, no loading state needed

  return <DashboardContent data={data} />;
}

Когда отображается Dashboard, функция useLoaderData() уже содержит необходимые данные, поэтому не требуется настройка состояния загрузки на уровне компонента или дополнительный код для обработки эффектов загрузки. Стоит знать два важных момента. Сама навигация ожидает завершения загрузки, поэтому всё равно необходим индикатор общего статуса ожидания, управляемый через useNavigation. Кроме того, поскольку Axios по умолчанию отклоняет ответы, не соответствующие коду 2xx, проверка response.status !== 200 в этом загрузчике никогда не фиксирует ошибку; отклонение ответа автоматически передаётся в элемент errorElement.

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

export async function dashboardLoader() {
  const criticalData = await api.get('/api/summary');
  const slowData = api.get('/api/analytics'); // Promise, not awaited

  return defer({
    summary: criticalData.data,
    analytics: slowData.then(r => r.data)
  });
}

Страница отображается с кратким содержанием в то время, как аналитические данные поступают, как правило, с использованием Await и Suspense. В React Router v7 параметр defer больше не требуется, и загрузчики могут напрямую возвращать обещания, поэтому обратитесь к документации для вашей версии.

Ошибка 5: ручное создание функций thunk для каждого запроса

Появление Redux Thunk стало значительным шагом вперед, но написание такой функции для каждого вызова API сегодня означает необходимость поддержания констант типов действий, функций создания действий, случаев обработки в редьюсере и флагов состояния для задач, которые современные инструменты могут выполнять декларативно:

// ❌ Bad: Thunk boilerplate for simple fetching
const FETCH_USER_REQUEST = 'FETCH_USER_REQUEST';
const FETCH_USER_SUCCESS = 'FETCH_USER_SUCCESS';
const FETCH_USER_FAILURE = 'FETCH_USER_FAILURE';
const fetchUser = (userId) => async (dispatch) => {
  dispatch({ type: FETCH_USER_REQUEST });
  try {
    const res = await api.get(`/users/${userId}`);
    dispatch({ type: FETCH_USER_SUCCESS, payload: res.data });
  } catch (error) {
    dispatch({ type: FETCH_USER_FAILURE, payload: error.message });
  }
};
// Reducer boilerplate...
// Component mapStateToProps...

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

RTK Query и TanStack Query предоставляют функции кэширования, удаления дублирующихся запросов, фонового обновления данных и управления состоянием загрузки из коробки. С помощью RTK Query вы описываете базовый запрос, включая хук prepareHeaders для добавления токена, а также указываете конечные точки; для вас автоматически генерируются хуки вроде useGetUserQuery и useUpdateUserMutation:

// ✅ Good: RTK Query handles the middleware
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const userApi = createApi({
  reducerPath: 'userApi',
  baseQuery: fetchBaseQuery({
    baseUrl: '/api',
    prepareHeaders: (headers) => {
      const token = localStorage.getItem('token');
      if (token) headers.set('authorization', `Bearer ${token}`);
      return headers;
    }
  }),
  endpoints: (builder) => ({
    getUser: builder.query({
      query: (id) => `/users/${id}`,
    }),
    updateUser: builder.mutation({
      query: ({ id, ...body }) => ({
        url: `/users/${id}`,
        method: 'PUT',
        body,
      }),
    }),
  }),
});
export const { useGetUserQuery, useUpdateUserMutation } = userApi;

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

Мидлвары Redux и механизмы безопасности интерфейса

Ошибка 6: изменение состояния внутри мидлваров Redux

Пользовательские мидлвары Redux — отличное место для логгирования, аналитики и преобразования действий. Однако это неправильное место для изменения состояния. Мидлвары выполняются между вызовом dispatch и редьюсером, поэтому изменения состояния там обходят систему отслеживания изменений Redux, нарушают возможности отладки с использованием механизма временных путешествий и создают путаницу в инструментах DevTools:

// ❌ Bad: State mutation in middleware
const analyticsMiddleware = store => next => action => {
  if (action.type === 'ADD_TO_CART') {
    // Directly mutating state! Never do this.
    store.getState().cart.lastAddedAt = Date.now();
  }
  return next(action);
};

Эта мутация также не приводит к перерисовке ничего, поскольку Redux и его связки обнаруживают изменения путем сравнения ссылок. Изменение поля в существующем объекте оставляет ссылку неизменной, поэтому подписчики не видят обновлений. Проверка неизменяемости в режиме разработки Redux Toolkit выявит такой тип мутации, что является ещё одной причиной для её использования.

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

// ✅ Good: Middleware only intercepts and forwards
const analyticsMiddleware = store => next => action => {
  // Log the action before it reaches the reducer
  analytics.track(action.type, {
    payload: action.payload,
    timestamp: Date.now(),
    userId: store.getState().auth.userId,
  });

  // Optionally transform the action
  if (action.type === 'ADD_TO_CART') {
    action = {
      ...action,
      meta: { addedAt: Date.now() }
    };
  }

  return next(action); // Forward to reducer
};
// Reducer handles the state update
function cartReducer(state = initialState, action) {
  switch (action.type) {
    case 'ADD_TO_CART':
      return {
        ...state,
        items: [...state.items, action.payload],
        lastAddedAt: action.meta.addedAt, // Proper immutable update
      };
    default:
      return state;
  }
}

Обратите внимание на взаимосвязь: редьюсер считывает значение action.meta.addedAt, поэтому он зависит от установленного мидлвэра. Генерация временной метки в функции создания действия (или в обратном вызове prepare в Redux Toolkit) позволяет избежать такой скрытой зависимости. Удобное воображаемое сравнение: мидлвэр — это почтовый отдел, который может читать конверты и ставить марки, но никогда не открывает ваш банковский отчет, чтобы изменить сумму.

Ошибка 7: рассмотрение границ обработки ошибок как второстепенного момента

Во многих приложениях существует едина граница вокруг корня компонентов, или её вообще нет. Границы обработки ошибок функционируют как мидлвэр для дерева компонентов: они ловят ошибки отрисовки в их дочерних элементах и предотвращают тем, чтобы одна неисправность разрушила всё интерфейс. Без них некорректный ответ API, отсутствующее свойство или сбой в скрипте стороннего разработчика могут сделать всю страницу пустой.

// ❌ Bad: No error boundaries
function App() {
  return (
    <div>
      <Header />
      <Sidebar />
      <Dashboard /> {/* If this crashes, everything dies */}
      <Footer />
    </div>
  );
}

Устанавливайте границы на уровне макета и элементов интерфейса, чтобы сбой в виджете аналитики не приводил к потере возможности навигации. Граница — это компонент класса, который устанавливает состояние ошибки в методе getDerivedStateFromError, сообщает об ошибке в методе componentDidCatch и отображает альтернативный вариант; использование нескольких границ позволяет каждой области восстанавливаться самостоятельно:

// ✅ Good: Strategic boundary placement
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }
static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  componentDidCatch(error, errorInfo) {
    logErrorToService(error, errorInfo);
  }
  render() {
    if (this.state.hasError) {
      return this.props.fallback || <DefaultErrorUI error={this.state.error} />;
    }
    return this.props.children;
  }
}
function App() {
  return (
    <ErrorBoundary fallback={<GlobalErrorPage />}>
      <Header />
      <div className="layout">
        <ErrorBoundary fallback={<SidebarError />}>
          <Sidebar />
        </ErrorBoundary>
        <main>
          <ErrorBoundary fallback={<WidgetError />}>
            <AnalyticsWidget />
          </ErrorBoundary>
          <ErrorBoundary fallback={<WidgetError />}>
            <RecentActivity />
          </ErrorBoundary>
        </main>
      </div>
    </ErrorBoundary>
  );
}

Имейте в виду их ограничения: границы ловят ошибки, возникающие во время отрисовки, в методах жизненного цикла и в конструкторах ниже уровня этих границ, но не ошибки в обработчиках событий, в асинхронном коде, таком как таймеры или обещания без использования оператора await, и не ошибки в самой границе. Для их обработки требуются обычные конструкции try/catch или глобальные обработчики. Если вы используете фреймворк с серверными компонентами, сочетайте клиентские границы с файлами обработки ошибок на уровне маршрутов (например, error.js в Next.js App Router), чтобы ловить ошибки сервера для каждого маршрута и ошибки клиента для каждого компонента.

Производительность и платформенные ограничения

Ошибка 8: тяжелая обработка в интерцепторах

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

// ❌ Bad: Heavy work in interceptor
api.interceptors.response.use((response) => {
  // This blocks rendering for large payloads
  response.data = heavyDecryptAndTransform(response.data);
  return response;
});

Преобразование, занимающее 200 миллисекунд, — это период в 200 миллисекунд, в течение которого страница игнорирует ввод, что на телефоне создаёт ощущение замороженного приложения. Перенесите тяжёлую обработку в Web Worker или отложите всё незначимое. Здесь ответы экспортируются в работник, а интерцептор ожидает результата:

// ✅ Good: Async offloading for heavy work
api.interceptors.response.use(async (response) => {
  if (response.config.url.includes('/export')) {
    // Move heavy work off main thread
    const worker = new Worker(new URL('./transform.worker.js', import.meta.url));

    const transformed = await new Promise((resolve, reject) => {
      worker.onmessage = (e) => resolve(e.data);
      worker.onerror = reject;
      worker.postMessage(response.data);
    });

    worker.terminate();
    response.data = transformed;
  }

  return response;
});

Создание нового рабочего процесса для каждого ответа сопряжено с начальными затратами, а функция postMessage копирует данные с использованием структурированного клонирования, поэтому при частых вызовах следует использовать рабочий процесс с длительным сроком жизни и рассмотреть возможность использования передаваемых буферов. Для шифрования или сжатия использование Web Crypto API внутри рабочего процесса или перенос вычислений на сервер помогает сохранять отзывчивость интерфейса. Более подробно об этом компромиссе см. когда следует переносить вычисления CPU в Web Worker.

Ошибка 9: рассматривание промежуточных компонентов Next.js как код приложения

Мидлвэры Next.js традиционно работают в среде Edge Runtime — изолированной среде V8, а не в полноценном Node.js. Они не могут получать доступ к контексту React или состоянию клиента, к тому же там отсутствует стандартный пул подключений к базе данных. Тем не менее часто в файле middleware.ts встречается сложная валидация, запросы к базе данных или логика, зависящая от состояния:

// ❌ Bad: Trying to run React logic in Edge Middleware
// middleware.ts
import { NextResponse } from 'next/server';
export async function middleware(request) {
  // This won't work: no access to React context
  const user = await db.user.findUnique(...); // No DB connection in Edge!

  // This is risky: heavy computation in Edge
  const result = complexAlgorithm(request.body);

  return NextResponse.next();
}

Ограничьтесь легкими операциями на уровне запросов: перезаписью данных, простыми проверками наличия куки, разделением на группы типа A/B, фильтрацией ботов и маршрутизацией в зависимости от геолокации. В этом примере непроверенные посетители перенаправляются сразу с /dashboard, в приложение передается заголовок эксперимента, а функция matcher используется только для обработки тех путей, где это необходимо:

// ✅ Good: Lightweight edge middleware
import { NextResponse } from 'next/server';
export function middleware(request) {
  const token = request.cookies.get('token')?.value;

  // Simple auth check: presence only
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  // A/B testing: lightweight header injection
  const experiment = request.cookies.get('exp')?.value || 'control';
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('x-experiment', experiment);

  return NextResponse.next({
    request: { headers: requestHeaders }
  });
}
export const config = {
  matcher: ['/dashboard/:path*', '/api/protected/:path*']
};

Проверка реальных сессий должна выполняться в обработчиках маршрутов или серверных компонентах с полным доступом к серверу; мидлвэр — это лишь входной портал, а не центр управления. Поддержка во время выполнения постоянно меняется: в последних версиях Next.js появилась опция использования среды выполнения Node.js для мидлвэра, а также были изменены правила названия файлов, поэтому необходимо уточнить текущее поведение в документации Next.js для вашей версии.

Ошибка 10: чрезмерная сложность для небольшого приложения

Не каждому проекту нужен набор интерцепторов, пользовательского мидлвэра Redux и рабочих процессов. Легко найти сайты-портфолио из трех страниц с сложными цепочками интерцепторов, ограничениями для статического текста и полным слоем обработки запросов только для одной формы контакта:

// ❌ Bad: Over-engineered simple app
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: Infinity,
      cacheTime: Infinity,
      retry: 3,
      retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
      refetchOnWindowFocus: true,
      refetchOnReconnect: true,
      // ... 20 more options for a static site
    }
  }
});

Это культ карго, а не архитектура: каждый слой добавляет дополнительную нагрузку на обработку данных, увеличивает вес пакетов и создаёт косвенные пути при отладке. Приложению с несколькими маршрутами и одним запросом не нужна стратегия кэширования. Конфигурация также содержит внутренние противоречия: при использовании параметра staleTime: Infinity данные никогда не считаются устаревшими, поэтому флаги фокусировки и повторной загрузки после восстановления соединения не выполняют никаких действий. Кроме того, в версии TanStack Query v5 параметр cacheTime был переименован в gcTime.

Масштабируйте инфраструктуру вместе с приложением:

  • Статические или простые сайты: вызывайте fetch напрямую, без промежуточных компонентов.
  • Небольшие приложения с механизмами аутентификации: один промежуточный компонент для обработки токенов и один — для реагирования на ошибки 401.
  • Средние приложения с маршрутизацией: добавьте загрузчики маршрутизатора и границы обработки ошибок на уровне лейаута.
  • Большие приложения с сложным состоянием: используйте RTK Query или TanStack Query вместе с пользовательским мидлвэром.

Умение определить, когда не следует добавлять абстракцию, — часть работы разработчика.

Подход к мидлвэру для React-приложений

Мидлвэр — это не только концепция, связанная с бэкендом. В React он представляет собой слой, который позволяет компонентам сосредотачиваться на отрисовке, в то время как общие задачи обрабатываются один раз.

  • Реализуйте авторизацию и преобразование ошибок в слое транспортировки, на одном экземпляре клиента, и всегда возвращайте результаты или отклоняйте запросы через интерцепторы.
  • Загружайте данные маршрутов с помощью загрузчиков или кэша запросов, а не через эффекты компонентов.
  • Ограничьтесь только побочными эффектами мидлвэра Redux и позвольте редьюсерам управлять изменениями состояния.
  • Используйте несколько границ обработки ошибок и помните, что они не могут перехватить все возможные ситуации.
  • Переносите тяжелые вычисления с главного потока и делайте мидлвэр на граничных уровнях максимально простым.
  • Добавляйте каждый слой только тогда, когда размер приложения этого требует.