Startseite / Artikel

React Auth, Datenladen und Fehler: Axios, React Router, Redux, TanStack Query

Zehn React-Fehler bei Authentifizierung, Datenladung und Fehlern, die durch Axios-Interceptoren, React Router-Loader, Redux-Middleware, TanStack Query sowie Error-Boundaries behoben werden können.

Verlag: REACTAPP.TOP15 Min. Lesezeit3360 Wörter

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

Auf der Backend-Seite überprüft niemand die Authentifizierung in jedem Route-Handler oder fügt Logging in jeden Controller ein; diese Aufgaben werden im Middleware abgewickelt, das einmal installiert wird und auf jede Anfrage angewendet wird. React-Codebasen verlieren oft diese Disziplin. Komponenten übernehmen schließlich selbst die Handhabung der Token-Einbindung, 401-Umleitungen, Fehlerprotokollierung sowie des Datenerhalts, was zu Boilerplate-Code, inkonsistentem Verhalten und Fehlern führt, die erst dann auftreten, wenn ein Benutzer die Elemente in einer bestimmten Reihenfolge anklickt. Eine React-App verfügt über ihr eigenes Anfragen-Lebenszyklus-Modell, bei dem API-Aufrufe, Route-Übergänge und Zustandsänderungen alle durch Schichten fließen, an die man sich anbinden kann. Diese Anleitung behandelt zehn häufige Fehler bei Axios-Interceptoren, React Router-Loadern, Zustandsbibliotheken, Error-Boundaries sowie Next.js-Middleware und zeigt, wo sich jede Aufgabe stattdessen befinden sollte.

Transportfragen: Authentifizierung und Axios-Interceptoren

Fehler 1: Behandlung von 401-Antworten in jedem Komponenten

Die Handhabung der Authentifizierung ist die häufigste Ursache für Überfüllung der Komponenten. Jede Fetch-Funktion enthält ihren eigenen catch-Block, der nach einem 401-Fehler sucht, den Speicher leert und umleitet:

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

Ausserhalb der Wiederholungen ist diese Lösung anfällig. Der Umstieg von lokaler Speicherung auf Cookies bedeutet, jeden Aufrufort zu bearbeiten. Die Hinzufügung einer Token-Rotation vor der Umleitung erfordert erneut die Anpassung aller Dateien. Zudem führt ein vergessener Überprüfungsschritt dazu, dass die App in einem halb ausgeloggten Zustand bleibt.

Die Lösung besteht in einem Axios-Response-Interceptor auf einer gemeinsamen Client-Instanz, der jede Antwort vor allen Komponenten abfängt. Die untenstehende Version fügt außerdem das Token beim Verlassen des Systems hinzu und versucht bei einem 401-Fehler einmal, das Token zu erneuern (geschützt durch ein _retry-Flag, damit kein Endlosschleifen entsteht). Anschliessend wird die ursprüngliche Anfrage mit dem neuen Token erneut gesendet, und erst wenn auch diese Erneuerung fehlschlägt, wird auf das Löschen der Speicherung und die Umleitung zurückgegriffen:

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

Die Komponenten importieren nun einfach api und rufen es auf, ohne Kenntnis über die Handhabung von 401-Fehlern; ein einziger Interceptor ist nun der einzige Ort, an dem entschieden wird, wie die Authentifizierung wiederhergestellt wird. Es gelten drei Einschränkungen. process.env.REACT_APP_API_URL ist die Konvention von Create React App; in einem Vite-Projekt würde man stattdessen import.meta.env.VITE_API_URL verwenden. Wenn mehrere Anfragen gleichzeitig fehlschlagen, löst jede von ihnen ihren eigenen Neuladeprozess aus, was rotierende Refresh-Tokens ungültig machen kann; die Lösung dazu wird in why concurrent 401s log users out erläutert. Zudem machen die Speicherung von Tokens in localStorage diese anfällig für XSS-Sicherheitslücken, weshalb httpOnly-Cookies in Betracht gezogen werden sollten.

Fehler 2: Kein Wert aus einem Interceptor zurückgegeben

Die Abfanggeräte bilden eine Kette, und jeder Glied muss seinen Wert an das nächste weitergeben. Diese beiden vergessen Folgendes:

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

Der Effekt wird oft als hängende Anfrage beschrieben, doch in aktuellen Axios-Versionen ist das Versagen in der Regel direkter und genauso verwirrend. Ein Response-Interceptor, der nichts zurückgibt, löst die Promise des Aufrufers mit undefined auf, wodurch const { data } = await api.get('/user') einen Dekonstruktionfehler auslöst, der weit von der eigentlichen Ursache entfernt ist. Ein Request-Interceptor, der nichts zurückgibt, lässt Axios ohne Konfiguration zurück, wodurch die Anfrage bereits vor dem Versand fehlschlägt. In jedem Fall tritt das Problem an einer anderen Stelle auf als dort, wo es ursprünglich entstanden ist.

Geben Sie immer das zurück, was Sie erhalten haben, und lehnen Sie in Error-Handlern explizit ab, damit der catch-Block des Aufrufers dennoch ausgeführt wird:

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

Die Analogie zu Express gilt weiterhin: Das Vergessen von next() blockiert eine Express-Anfrage, und das Vergessen von return unterbricht eine Interceptor-Kette.

Fehler 3: Manuelles Hinzufügen von Auth-Headern

Das Kopieren eines Authorization-Headers in jeden Aufruf behandelt ein durchgängiges Problem als lokales Problem. Früher oder später wird der Header weggelassen, falsch formatiert oder Bearer null gesendet. Der folgende Codeausschnitt zeigt außerdem, wie leicht man Fehler macht: Bei dem post-Aufruf wird das Header-Objekt als Anfragekörper und die Payload als Konfiguration übergeben, weil Axios post(url, data, config) erwartet:

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

Ein Anfragen-Interceptor beseitigt die gesamte Fehlerklasse, indem er den Header an einer Stelle hinzufügt – und zwar nur dann, wenn ein Token vorhanden ist:

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

Falls Sie TanStack Query verwenden, bieten seine Caches ebenfalls globale Hooks für Nebeneffekte. Ein QueryCache onError kann bei 401-Fehlern für jede Abfrage umleiten, und ein MutationCache onError kann bei jeder fehlgeschlagenen Mutation eine Benachrichtigung anzeigen:

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

Das zugrundeliegende Prinzip ist, dass die Authentifizierung eine Transportfrage ist und nicht eine Komponentenfrage, weshalb sie auf der Transportschicht behandelt werden sollte.

Datenladen: React Router Loader und TanStack Query-Caches

Fehler 4: Datenabruf in useEffect anstelle eines Route-Loaders

Das bekannte Muster besteht darin, die Komponente zu initialisieren, einen Effekt auszuführen, einen Ladeindikator anzuzeigen, die Daten zu erhalten und anschließend zu rendern:

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

Dadurch entstehen Anfragen in Abfolgen: Der Router rendernt das Komponente, erst danach startet der Effekt die Anfrage, und erst nach deren Rückkehr wird der Zustand aktualisiert. Das Hin- und Herwechseln lädt jeweils neu. Ältere React-Versionen warnten außerdem vor der Zustandsänderung nach dem Entfernen der Komponente; React 18 hat diese Warnung entfernt, doch das zugrunde liegende Problem zwischen langsamen und schnelleren Antworten besteht weiterhin.

Die Daten-APIs von React Router (seit v6.4 mit createBrowserRouter verfügbar) ermöglichen es einer Route, einen loader anzugeben, der vor dem Rendern des Route-Elements Daten lädt, sowie einen errorElement, der angezeigt wird, wenn der Loader einen Fehler auslöst:

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

Wenn das Dashboard gerendert wird, verfügt useLoaderData() bereits über die Daten, sodass es keinen Ladezustand auf Komponentenebene oder Boilerplate für Effekte gibt. Zwei Punkte sind erwähnenswert: Die Navigation selbst wartet auf den Lader, weshalb man weiterhin einen globalen Statusanzeiger benötigt, der von useNavigation gesteuert wird. Da Axios standardmäßig Antworten außerhalb der 2xx-Kategorie ablehnt, erkennt die Prüfung response.status !== 200 in diesem Lader niemals einen Fehlerzustand; die Ablehnung wird automatisch an errorElement weitergeleitet.

Für sekundäre Daten, die die Seite nicht blockieren sollten, kann der Lader den kritischen Anfragevorgang abwarten und den langsamen Anfragevorgang mithilfe von defer als unaufgelöste Promise zurückgeben:

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

Die Seite wird mit dem Zusammenfassungstext angezeigt, während gleichzeitig die Analysedaten eingeholt werden, in der Regel über Await und Suspense. In React Router v7 ist defer nicht mehr erforderlich, und Loader können direkt Promises zurückgeben – prüfen Sie daher die Dokumentation für die von Ihnen verwendete Version.

Fehler 5: Handgeschriebene Thunks für jede Anfrage

Redux Thunk war ein großer Schritt nach vorn, als er eingeführt wurde, doch das Schreiben eines Thunks pro API-Aufruf bedeutet heute, dass man Action-Typ-Konstanten, Action-Creator, Reducer-Fälle sowie Status-Flags pflegen muss – obwohl moderne Tools diese Aufgaben deklarativ bewältigen können:

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

Diese Struktur war vernünftig, als es nichts Besseres gab. Für herkömmliche CRUD-Aufgaben ist sie heute überflüssig.

RTK Query und TanStack Query bieten standardmäßig Caching, Deduplizierung von Anfragen, Hintergrundaufrufe sowie Ladezustände. Mit RTK Query definieren Sie eine Basisanfrage, einschließlich eines prepareHeaders-Hooks, der das Token hinzufügt, und deklarieren Endpunkte; Hooks wie useGetUserQuery und useUpdateUserMutation werden automatisch für Sie generiert:

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

Hinter diesen Hooks speichert RTK Query Antworten pro Endpunkt und Argument, zählt, wie viele geladene Komponenten jede Eintragung verwenden, und entfernt Einträge, die niemand mehr nutzt. Beachten Sie, dass es Entitäten nicht auf die gleiche Weise normalisiert wie ein manuell erstellter Entity-Adapter; die gespeicherten Ergebnisse werden genau so abgelegt, wie sie zurückgegeben werden. In jedem Fall schreiben Sie die Geschäftslogik, während die Bibliothek die zugrundeliegenden Abläufe übernimmt.

Redux-Middleware und Sicherheitsmechanismen für die Benutzeroberfläche

Fehler 6: Zustand innerhalb des Redux-Middlewares verändern

Custom-Redux-Middleware eignet sich gut für Logging, Analytik und Transformation von Aktionen. Es ist jedoch der falsche Ort, um den Zustand zu ändern. Middleware läuft zwischen dispatch und dem Reducer, wodurch eine Änderung des Zustands dort das Change-Tracking von Redux umgeht, das Debuggen mit Zeitreisefunktionen stört und die DevTools verwirrt:

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

Diese Mutation führt außerdem nicht zu einer Neurenderung, da Redux und seine Bindungen Änderungen durch Vergleich von Referenzen erkennen. Das Ändern eines Feldes im vorhandenen Objekt lässt die Referenz unverändert, weshalb die Abonnenten keine Aktualisierung wahrnehmen. Die Immutabilitätsprüfung im Entwicklungsmodus von Redux Toolkit weist auf diese Art von Mutation hin – was ein weiterer Grund dafür ist, es zu verwenden.

Beschränken Sie Middleware auf das Lesen, Protokollieren, Transformieren oder Weiterleiten von Aktionen und bewahren Sie alle Zustandsänderungen in Reduzern auf. Hier verfolgt die Middleware die Aktion und fügt einen Zeitstempel als meta hinzu, während der Reducer mithilfe dessen eine immutable Aktualisierung vornimmt:

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

Achten Sie auf die Abhängigkeit: Der Reducer liest action.meta.addedAt, wodurch er von der installierten Middleware abhängt. Die Erstellung des Zeitstempels im Action-Creator (oder in einem prepare-Callback in Redux Toolkit) vermeidet diese versteckte Abhängigkeit. Ein nützliches Bild: Die Middleware ist wie ein Postbüro, das Umschläge lesen und Stempel aufkleben kann, aber niemals Ihren Bankauszug öffnet, um den Saldo zu ändern.

Fehler 7: Fehlergrenzen als Nachgedanke betrachten

Viele Anwendungen haben nur eine einzige Fehlergrenze um die Wurzelkomponente oder gar keine. Fehlergrenzen fungieren wie Middleware für den Komponentenbaum: Sie fangen Renderungsfehler in ihren Kindern ab und verhindern, dass ein einziger Fehler die gesamte Benutzeroberfläche lahmlegt. Ohne sie kann eine fehlerhafte API-Antwort, ein fehlendes Attribut oder ein fehlerhafter Drittanbieter-Script die gesamte Seite leer machen:

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

Setzen Sie Grenzen auf Layout- und Feature-Ebene, damit ein Fehler im Analytics-Widget die Navigation nicht beeinträchtigt. Eine Grenze ist ein Klassenkomponente, die den Fehlerzustand in getDerivedStateFromError festlegt, in componentDidCatch Bericht erstattet und einen Ersatz anzeigt; durch das Nesten mehrerer solcher Grenzen erhält jede Region ihre eigene Wiederherstellungsmöglichkeit:

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

Halten Sie ihre Grenzen im Hinterkopf: Die Grenzen fangen Fehler auf, die während der Darstellung, in den Lebenszyklusmethoden sowie in den Konstruktoren darunter auftreten, aber nicht Fehler in Event-Handlern, in asynchronem Code wie Timern oder unawaited Promises oder in der Grenze selbst. Für diese Fälle sind gewöhnliche try/catch-Blöcke oder globale Handler erforderlich. Wenn Sie ein Framework mit Server Components verwenden, kombinieren Sie die clientseitigen Grenzen mit den fehlerbehandelnden Dateien auf Route-Ebene (zum Beispiel error.js im Next.js App Router), um Serverfehler pro Route und Clientfehler pro Komponente abzufangen.

Leistung und Plattformgrenzen

Fehler 8: Aufwendige Verarbeitung in Interceptoren

Interceptoren sind im Sinne von Promises asynchron, doch ihr Code läuft dennoch auf dem Hauptthread. Aufwendige Entschlüsselungs-, Umwandlungs- oder Parservorgänge dort blockieren die Darstellung:

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

Eine Umwandlungsdauer von 200 Millisekunden bedeutet, dass die Seite 200 Millisekunden lang Eingaben ignoriert, was auf einem Handy wie ein eingefrorenes Programm wirkt. Verlagern Sie aufwändige Verarbeitungsaufgaben in einen Web Worker oder verschieben Sie alles Unwesentliche. Hier werden die Exportantworten an einen Worker gesendet und der Interceptor wartet auf das Ergebnis:

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

Die Erstellung eines neuen Workers pro Antwort verursacht Startkosten, und postMessage kopiert Daten mittels strukturierter Klonierung. Daher sollte bei häufigen Aufrufen ein langlebiger Worker wiederverwendet werden, und man sollte über übertragbare Puffer nachdenken. Für Verschlüsselung oder Kompression hilft die Web Crypto API innerhalb eines Workers oder der Umzug der Arbeit auf den Server, damit die Benutzeroberfläche reaktiv bleibt. Weitere Informationen zu diesem Kompromiss finden Sie unter wann CPU-Arbeit in einen Web Worker verlagert werden sollte.

Fehler 9: Next.js-Middleware wie Anwendungscode behandeln

Traditionell wurde das Next.js-Middleware im Edge-Runtime ausgeführt, einem V8-Isolat anstelle eines vollständigen Node.js. Es kann weder den React-Context noch den Client-Zustand erkennen, und ein regulärer Datenbankverbindungspool steht dort nicht zur Verfügung. Dennoch ist es üblich, in middleware.ts umfangreiche Validierungen, Datenbankabfragen oder zustandsbasierte Logik zu finden:

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

Beschränken Sie sich auf leichte Aufgaben auf Ebene der Anfragen: Umformulierungen, günstige Überprüfungen der Cookie-Präsenz, A/B-Gruppierung, Filterung von Bots sowie routenbasiertes Verhalten je nach Standort. Dieses Beispiel leitet unauthentifizierte Besucher von /dashboard ab, leitet einen Experiment-Header an die Anwendung weiter und verwendet matcher, um nur auf denjenigen Pfaden auszuführen, die dies benötigen:

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

Die echte Sitzungsprüfung sollte in Route-Handlern oder Server Components mit vollständigem Server-Zugriff erfolgen; Middleware dient lediglich als Eingangskontrolle, nicht als zentrales Steuerungssystem. Die Laufzeitunterstützung ändert sich ständig: Neuere Next.js-Versionen haben eine Node.js-Laufzeitoption für Middleware hinzugefügt und die Dateikonventionen überarbeitet, daher sollten Sie das aktuelle Verhalten in den Next.js-Dokumenten für Ihre Version überprüfen.

Fehler 10: Überengineering einer kleinen Anwendung

Nicht jedes Projekt benötigt eine Reihe von Interzeptoren, benutzerdefinierte Redux-Middleware sowie Worker-Prozesse. Es gibt zahlreiche dreiseitige Portfolio-Webseiten mit aufwendigen Interzeptor-Ketten, Grenzen für statischen Text sowie einer umfangreichen Abfrageschicht nur für ein Kontaktformular.

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

Das ist Cargo Culting, keine Architektur: Jede Schicht führt beim Debuggen zu zusätzlichem mentalen Aufwand, höherem Paketgewicht und Indirektheit. Eine App mit ein paar Routen und einer einzigen Anfrage benötigt keine Caching-Strategie. Die Konfiguration ist außerdem intern inkonsistent: Mit staleTime: Infinity wird Daten niemals als veraltet angesehen, wodurch die Flags für Fokussierung und erneutes Abrufen keine Wirkung zeigen. Zudem wurde cacheTime in TanStack Query v5 in gcTime umbenannt.

Skalieren Sie die Infrastruktur mit der App:

  • Statische oder einfache Websites: Rufen Sie fetch direkt auf, ohne Interceptor.
  • Kleine Apps mit Authentifizierung: Ein Anfrage-Interceptor für Token und ein Antwort-Interceptor für 401-Fehler.
  • Mittlere Apps mit Routing: Fügen Sie Router-Loader sowie Fehlerbegrenzungen auf Layout-Ebene hinzu.
  • Große Apps mit komplexem Zustand: Nutzen Sie RTK Query oder TanStack Query sowie benutzerdefiniertes Middleware.

Es gehört zum Job, zu wissen, wann man keine Abstraktion hinzufügen sollte.

Middleware-Denken für React-Apps

Middleware ist nicht nur eine Idee für den Backend-Bereich. In React ist es die Schicht, die die Komponenten darauf konzentriert, Inhalte anzuzeigen, während gemeinsame Probleme nur einmal gelöst werden.

  • Platzieren Sie die Authentifizierung und die Umwandlung von Fehlern in die Transportschicht, auf einer einzigen Client-Instanz, und geben Sie diese immer über Interceptor zurück oder lehnen Sie sie ab.
  • Laden Sie Routendaten in Loadern oder in einem Query-Cache statt in den Component-Effects.
  • Beschränken Sie die Nebenwirkungen des Redux-Middleware auf das Notwendige und überlassen Sie es den Reduzern, Zustandsänderungen vorzunehmen.
  • Verwenden Sie mehrere Fehlergrenzen und bedenken Sie, was diese nicht abfangen können.
  • Halten Sie aufwendige Aufgaben vom Hauptthread fern und halten Sie das Edge-Middleware schlank.
  • Fügen Sie jede Schicht nur dann hinzu, wenn die Größe der Anwendung dies rechtfertigt.