Accueil / Articles

Authentification React, chargement des données et erreurs : Axios, React Router, Redux, TanStack Query

Dix erreurs courantes dans React liées à l’authentification, au chargement des données et aux erreurs, corrigées grâce aux intercepteurs d’Axios, aux chargeurs de React Router, aux middleware de Redux, à TanStack Query et aux limites d’erreur.

Éditeur : REACTAPP.TOP15 min de lecture3360 mots

Sources: Stop Polluting Your Components: The React ‘Middleware’ Guide (Axios, Routers, and State)

En arrière-plan, personne ne vérifie l’authentification dans chaque gestionnaire de route ni n’insère de fonctionnalités de journalisation dans chaque contrôleur ; ce travail est géré par le middleware, qui est chargé une seule fois et appliqué à chaque requête. Les projets React perdent souvent cette discipline. Les composants finissent par gérer eux-mêmes l’injection des tokens, les redirections 401, le journalage des erreurs et la récupération des données, ce qui génère du code redondant, un comportement incohérent et des bugs qui ne se manifestent que lorsque l’utilisateur clique sur les éléments dans un ordre particulier. Une application React dispose de son propre cycle de vie des requêtes, avec des appels API, des transitions de route et des mises à jour d’état qui s’effectuent tous à travers des couches auxquelles on peut se connecter. Ce guide aborde dix erreurs courantes liées aux intercepteurs d’Axios, aux chargeurs de React Router, aux bibliothèques d’état, aux limites d’erreur et au middleware de Next.js, et montre où chaque fonctionnalité devrait plutôt être placée.

Problématiques de transport : authentification et intercepteurs d’Axios

Erreur 1 : gestion des réponses 401 dans chaque composant

La gestion de l’authentification est la cause la plus fréquente de surcharge des composants. Chaque fonction fetch dispose de son propre bloc catch qui vérifie la présence d’une réponse 401, efface les données en mémoire et redirige l’utilisateur :

// ❌ 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;
  }
}

En dehors de la répétition, cette solution est fragile. Passer du stockage local aux cookies signifie devoir modifier chaque endroit où une requête est envoyée. Ajouter une rotation des tokens de mise à jour avant le redirigement implique de modifier à nouveau tous les fichiers. De plus, une vérification oubliée laisse l’application dans un état partiellement déconnecté.

La solution consiste à utiliser un intercepteur de réponse Axios sur une instance de client partagée, qui traite chaque réponse avant que n’interviennent les composants. La version présentée ci-dessous ajoute également le token au moment du départ, et en cas de code 401, elle tente une mise à jour une fois (protégée par un drapeau _retry pour éviter les boucles), renvoie la requête originale avec le nouveau token, et ne recourt qu’au nettoyage du stockage et au redirigement si la mise à jour échoue elle-même :

// 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;

Les composants n’ont désormais qu’à importer api et l’appeler, sans tenir compte du traitement des erreurs 401 ; un seul intercepteur détermine désormais comment se faire la récupération de l’authentification. Trois précautions s’imposent. process.env.REACT_APP_API_URL correspond à la convention de Create React App ; dans un projet Vite, on utilisera plutôt import.meta.env.VITE_API_URL. Si plusieurs requêtes échouent en même temps, chacune déclenche son propre renouvellement, ce qui peut rendre invalides les tokens de renouvellement rotatifs ; la solution est abordée dans pourquoi les erreurs 401 simultanées déconnectent les utilisateurs. De plus, conserver les tokens dans localStorage les expose à tout bug XSS, il est donc judicieux d’envisager l’utilisation de cookies httpOnly.

Erreure 2 : ne pas retourner de valeur depuis un intercepteur

Les intercepteurs forment une chaîne, et chaque maillon doit transmettre sa valeur au suivant. Ces deux-là oublient de le faire :

// ❌ 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!
});

L’effet est souvent décrit comme un blocage de la requête, mais dans les versions actuelles d’Axios, l’échec est généralement plus direct et tout aussi déroutant. Un intercepteur de réponse qui ne renvoie rien résout la promesse de l’appelant avec undefined, de sorte que const { data } = await api.get('/user') génère une erreur de déstructuration éloignée de sa cause réelle. Un intercepteur de requête qui ne renvoie rien laisse Axios sans configuration, et la requête échoue avant même d’être envoyée. Dans les deux cas, le bug apparaît ailleurs que là où il a été écrit.

Renvoyez toujours ce que vous avez reçu, et rejetez explicitement dans les gestionneurs d’erreurs afin que le catch de l’appelant s’exécute toujours :

// ✅ 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);
  }
);

L’analogie avec Express reste valable : oublier next() bloque une requête Express, et oublier return interrompt une chaîne d’intercepteurs.

Erreur 3 : ajouter manuellement les en-têtes d’authentification

Copier un en-tête Authorization dans chaque appel traite un problème transversal comme s’il était local. Tôt ou tard, un appel l’omet, le formate différemment ou envoie Bearer null. Le fragment ci-dessous montre également à quel point il est facile de se tromper : dans l’appel post, l’objet d’en-tête est passé en tant que corps de la requête et la charge utile en tant que configuration, car Axios attend 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}` } });

Un intercepteur de requête élimine toute la catégorie d’erreurs en ajoutant l’en-tête en un seul endroit, uniquement lorsque un jeton existe :

// ✅ 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;
});

Si vous utilisez TanStack Query, ses caches offrent également des hooks globaux pour gérer les effets secondaires. Un QueryCache onError peut rediriger en cas de code 401 pour toute requête, et un MutationCache onError peut afficher un message d’erreur pour toute mutation échouée :

// ✅ 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);
    }
  })
});

Le principe fondamental est que l’authentification relève du domaine du transport et non de celui des composants, il convient donc de la gérer au niveau de la couche de transport.

Chargement des données : chargeurs React Router et caches TanStack Query

Erreur 4 : effectuer des requêtes avec useEffect au lieu d’utiliser un chargeur de route

Le schéma habituel consiste à initialiser le composant, exécuter un effet, afficher un indicateur de chargement, recevoir les données, puis les afficher :

// ❌ 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} />;
}

Cela génère des cascades de requêtes : le routeur rend le composant, ce n’est qu’alors que l’effet lance la requête, et ce n’est qu’après son retour que l’état est mis à jour. Naviguer d’avant en arrière provoque un rechargement à chaque fois. Les versions plus anciennes de React avertissaient également contre la modification de l’état après le démontage ; React 18 a supprimé cet avertissement, mais le conflit entre une réponse lente et une réponse plus rapide reste réel.

Les API de données de React Router (disponibles avec createBrowserRouter depuis v6.4) permettent à une route de déclarer un loader qui effectue des requêtes avant que l’élément de route ne soit rendu, ainsi qu’un errorElement qui s’affiche si le loader lance une exception :

// ✅ 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} />;
}

Lorsque le Dashboard est affiché, useLoaderData() dispose déjà des données, il n’y a donc pas de état de chargement au niveau du composant ni de code générique associé. Deux points méritent d’être notés. La navigation elle-même attend le chargement, ce qui explique pourquoi vous avez encore besoin d’un indicateur global de traitement en cours géré par useNavigation. De plus, comme Axios rejette par défaut les réponses autres que 2xx, la vérification response.status !== 200 dans ce chargement ne détecte jamais un statut d’erreur ; le rejet est transmis automatiquement à errorElement.

Pour les données secondaires qui ne doivent pas bloquer la page, le chargement peut attendre la requête critique et retourner la requête lente sous forme de promesse non résolue en utilisant 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)
  });
}

La page s’affiche avec le résumé tandis que les données analytiques sont chargées, généralement via Await et Suspense. Dans React Router v7, defer n’est plus requis et les chargeurs peuvent retourner directement des promesses ; veuillez donc consulter la documentation correspondant à la version que vous utilisez.

Erreur 5 : création manuelle de thunks pour chaque requête

Redux Thunk a représenté un grand progrès à son lancement, mais écrire un thunk pour chaque appel API aujourd’hui signifie devoir gérer des constantes de type d’action, des créateurs d’action, des cas de réducteur et des indicateurs d’état pour des tâches que les outils modernes peuvent gérer de manière déclarative :

// ❌ 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...

Cette structure était raisonnable lorsque rien de mieux n’existait. Pour les opérations CRUD standard, elle est désormais inutile.

RTK Query et TanStack Query offrent par défaut du cache, de la déduplication des requêtes, un rechargement en arrière-plan ainsi que des états de chargement. Avec RTK Query, vous décrivez une requête de base, y compris un hook prepareHeaders qui ajoute le token, et vous déclarez des points de terminaison ; des hooks tels que useGetUserQuery et useUpdateUserMutation sont générés automatiquement pour vous :

// ✅ 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;

Derrière ces mécanismes, RTK Query met en cache les réponses par point de terminaison et par argument, compte le nombre de composants utilisés pour chaque entrée et élimine celles qui ne sont plus utilisées par quiconque. Notez qu’il ne normalise pas les entités de la même manière qu’un adaptateur d’entités conçu manuellement ; les résultats en cache sont stockés tels qu’ils ont été retournés. Quoi qu’il en soit, c’est vous qui écrivez la logique métier, tandis que la bibliothèque s’occupe des aspects techniques.

Middleware Redux et filets de sécurité pour l’interface utilisateur

Erreur 6 : modifier l’état à l’intérieur du middleware Redux

Un middleware Redux personnalisé est un excellent endroit pour le journalage, les analyses et la transformation des actions. C’est cependant le mauvais endroit pour modifier l’état. Le middleware s’exécute entre l’appel à dispatch et le réducteur, donc écrire dans l’état à cet endroit contourne le suivi des modifications de Redux, perturbe le débogage par régression dans le temps et embrouille les outils 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);
};

La mutation échoue également à réafficher quoi que ce soit, car Redux et ses bindings détectent les changements en comparant des références. Modifier un champ de l’objet existant maintient la référence identique, de sorte que les abonnés ne voient aucune mise à jour. La vérification d’immuabilité en mode développement de Redux Toolkit signalera ce type de mutation, ce qui constitue une raison de plus d’en utiliser.

Restreignez les middleware à la lecture, à l’enregistrement, à la transformation ou à la redirection des actions, et conservez chaque modification d’état dans les reducers. Ici, le middleware suit l’action et y ajoute une date et une heure en tant que meta, tandis que le reducer effectue une mise à jour immuable en s’appuyant sur ces informations :

// ✅ 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;
  }
}

Prêtez attention au couplage : le réducteur lit action.meta.addedAt, il dépend donc du middleware installé. Générer l’heure de création dans le créateur d’action (ou dans une fonction de rappel prepare de Redux Toolkit) évite cette dépendance cachée. Une image mentale utile : le middleware est comme le bureau de poste qui peut lire les enveloppes et y apposer des timbres, mais il n’ouvre jamais votre relevé bancaire pour modifier le solde.

Erreur 7 : considérer les limites d’erreur comme une solution de dernier recours

De nombreuses applications disposent d’une seule limite autour de la racine, ou aucune. Les limites d’erreur fonctionnent comme du middleware pour l’arbre des composants : elles capturent les erreurs de rendu de leurs enfants et empêchent qu’une seule panne ne fasse planter toute l’interface. Sans elles, une réponse API mal formatée, une propriété manquante ou un script tiers défaillant peuvent rendre toute la page blanche :

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

Placez des limites au niveau du layout et des fonctionnalités afin qu’une panne du widget d’analyse ne supprime pas la navigation. Une limite est un composant de classe qui définit l’état d’erreur dans getDerivedStateFromError, envoie des rapports dans componentDidCatch et affiche un contenu de remplacement ; en en nesting plusieurs, chaque région dispose de sa propre capacité de récupération :

// ✅ 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>
  );
}

Tenez compte de leurs limites : les frontières captent les erreurs générées lors du rendu, dans les méthodes de cycle de vie et dans les constructeurs qui les précèdent, mais pas les erreurs dans les gestionnaires d’événements, dans le code asynchrone tel que les temporisateurs ou les promesses non attendues, ni dans la frontière elle-même. Ces dernières nécessitent des blocs try/catch ordinaires ou des gestionnaires globaux. Si vous utilisez un framework avec des composants serveur, combinez les frontières client avec les fichiers d’erreur au niveau des routes (par exemple error.js dans le Next.js App Router), afin de capturer les erreurs serveur par route et les erreurs client par composant.

Performance et frontières de plateforme

Erreur 8 : traitements lourds à l’intérieur des intercepteurs

Les intercepteurs sont asynchrones au sens des promesses, mais leur code s’exécute néanmoins sur le thread principal. Un déchiffrement, une transformation ou une analyse coûteux effectués là bloque le rendu :

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

Une transformation de 200 millisecondes correspond à 200 millisecondes pendant lesquelles la page ignore les entrées, ce qui, sur un téléphone, donne l’impression que l’application est figée. Déplacez le traitement lourd dans un Web Worker, ou reportez tout ce qui n’est pas essentiel. Ici, les réponses d’exportation sont envoyées à un worker et l’intercepteur attend le résultat :

// ✅ 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;
});

Créer un nouveau travailleur pour chaque réponse entraîne des coûts de démarrage, et postMessage copie les données par clonage structuré ; par conséquent, pour des appels fréquents, il convient de réutiliser un travailleur à longue durée de vie et d’envisager l’utilisation de buffers transférables. Pour le chiffrement ou la compression, l’API Web Crypto intégrée dans un travailleur, ou le transfert du travail vers le serveur, permet de maintenir l’interface utilisateur réactive. Pour en savoir plus sur cet équilibre, consultez quand transférer le travail du CPU vers un Web Worker.

Erreur 9 : traiter les middleware de Next.js comme du code d’application

Le middleware de Next.js s’exécutait traditionnellement sur le runtime Edge, un environnement isolé V8 plutôt que sur un Node.js complet. Il ne peut pas accéder au contexte React ni à l’état du client, et un pool de connexions de base de données classique n’est pas disponible. Néanmoins, il est courant de trouver des validations complexes, des requêtes à la base de données ou de la logique gérant de l’état dans 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();
}

Restez concentré sur des tâches légères au niveau de la requête : réécritures, vérifications simples de la présence de cookies, regroupement A/B, filtrage des bots et routage basé sur la géolocalisation. Cet exemple redirige les visiteurs non authentifiés vers une autre page que /dashboard, envoie une en-tête d’expérience à l’application, et utilise matcher pour ne s’exécuter que sur les chemins qui en ont besoin :

// ✅ 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*']
};

La validation réelle des sessions doit se faire dans les gestionnaires de route ou les composants serveur disposant d’un accès complet au serveur ; le middleware n’est que le portier, pas le bureau administratif. Le support en temps de exécution évolue constamment : les dernières versions de Next.js ont ajouté une option de runtime Node.js pour le middleware et ont modifié la convention de nommage des fichiers, il convient donc de vérifier le comportement actuel dans la documentation de Next.js correspondant à votre version.

Erreur 10 : sur-engineering d’une petite application

Chaque projet n’a pas besoin d’une pile d’intercepteurs, de middleware Redux personnalisés ou de workers. On trouve facilement des sites de portfolio à trois pages dotés de chaînes d’intercepteurs complexes, de limites autour du texte statique et d’une couche de requêtes complète pour un seul formulaire de contact :

// ❌ 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
    }
  }
});

C’est du culte du cargo, pas de l’architecture : chaque couche ajoute une charge mentale, un poids supplémentaire et des intermédiaires lors du débogage. Une application avec quelques routes et une seule requête n’a pas besoin de stratégie de mise en cache. La configuration est également intrinsèquement incohérente : avec staleTime: Infinity, les données ne sont jamais considérées comme obsolètes, de sorte que les flags focus et reconnect refetch ne servent à rien. De plus, cacheTime a été renommé gcTime dans TanStack Query v5.

Échellez les outils en fonction de l’application :

  • Sites statiques ou simples : appelez directement fetch, sans intercepteurs.
  • Petites applications avec authentification : un intercepteur de requête pour les tokens et un intercepteur de réponse pour les erreurs 401.
  • Applications moyennes avec routage : ajoutez des chargeurs de routeur et des limites d’erreur au niveau du layout.
  • Applications volumineuses avec un état complexe : utilisez RTK Query ou TanStack Query ainsi que du middleware personnalisé.

Savoir quand ne pas ajouter d’abstraction fait partie du métier.

Pensée middleware pour les applications React

Le middleware n’est pas seulement une idée de backend. Dans React, c’est la couche qui permet aux composants de se concentrer sur le rendu tandis que les problématiques communes sont gérées une seule fois.

  • Placiez l’authentification et la traduction des erreurs dans la couche de transport, sur une seule instance client, et retournez toujours ou rejetez depuis les intercepteurs.
  • Chargez les données des routes dans des chargeurs ou un cache de requêtes plutôt que dans les effets des composants.
  • Conservez uniquement les effets secondaires du middleware Redux et laissez les réducteurs gérer les modifications d’état.
  • Utilisez plusieurs limites d’erreur, et rappelez-vous de ce qu’elles ne peuvent pas capturer.
  • Éloignez le travail lourd du thread principal et maintenez un middleware léger pour les cas limites.
  • Ne ajoutez chaque couche que lorsque la taille de l’application l’exige vraiment.