Auth w React, ładowanie danych i błędy: Axios, React Router, Redux, TanStack Query
Te błędy w React dotyczą autoryzacji, ładowania danych i błędów; zostały naprawione za pomocą interceptorów Axios, loaderów React Router, middleware’a Redux, TanStack Query oraz granic błędów.
Źródła: Stop Polluting Your Components: The React ‘Middleware’ Guide (Axios, Routers, and State)
W warstwie backendowej nikt nie sprawdza autoryzacji w każdym obsługiwaczu tras ani nie wstawia logowania do każdego kontrolera; ta funkcja znajduje się w middleware, które jest konfigurowane raz i stosowane do każdego żądania. Bazy kodu React często tracą tę dyscyplinę. Komponenty muszą samodzielnie zajmować się wstrzykiwaniem tokenów, przekierowaniami 401, logowaniem błędów oraz pobieraniem danych, co skutkuje nadmiarem kodu, niespójnym zachowaniem oraz błędami, które pojawiają się tylko wtedy, gdy użytkownik kliknie elementy w określonej kolejności. Aplikacja React ma swój własny cykl życia żądania, w którym wywołania API, przejścia między trasami oraz aktualizacje stanu przechodzą przez warstwy, do których można się podłączyć. Ten przewodnik omawia dziesięć częstych błędów w interceptorach Axios, loaderach React Router, bibliotekach stanu, granicach błędów oraz middleware w Next.js i pokazuje, gdzie powinna znajdować się każda z tych funkcji.
Kwestie transportowe: autoryzacja i intercepcje Axios
Błąd 1: obsługa odpowiedzi 401 we wszystkich komponentach
Obsługa autoryzacji jest najczęstszą przyczyną nadmiaru kodu w komponentach. Każda funkcja fetch ma swój własny blok catch, który sprawdza kod 401, usuwa dane z pamięci i przekierowuje użytkownika:
// ❌ 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;
}
}
Poza powtarzalnością, rozwiązanie to jest kruche. Przeniesienie danych z lokalnego przechowywania na pliki cookie oznacza konieczność edycji każdego miejsca, w którym te dane są wykorzystywane. Dodanie mechanizmu rotacji tokenów odświeżania przed przekierowaniem wymaga ponownego edytowania każdego pliku. A jeden zapomniany sprawdzian może pozostawić aplikację w stanie częściowo wylogowanej.
Rozwiązaniem jest interceptor odpowiedzi Axios w wspólnej instancji klienta, który przetwarza każdą odpowiedź przed jakimkolwiek komponentem. Podana poniżej wersja dodatkowo przypina token przy wyjściu z aplikacji, a w przypadku błędu 401 próbuje odświeżyć go raz (pod kontrolą flagi _retry, aby uniknąć błędnego cyklu), ponownie wysyła oryginalną prośbę z nowym tokenem i dopiero wtedy, gdy sam proces odświeżania się nie powiedzie, przechodzi do usunięcia danych z przechowywania i przekierowania użytkownika.
// 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;
Komponenty teraz po prostu importują api i go wywołują, nie biorąc pod uwagę obsługi błędu 401; jeden interceptor jest teraz jedynym miejscem, które decyduje o tym, jak odzyskać autoryzację. Obowiązują trzy zastrzeżenia. process.env.REACT_APP_API_URL to konwencja Create React App; w projekcie Vite należy zamiast tego użyć import.meta.env.VITE_API_URL. Jeśli kilka żądań zawiedzie jednocześnie, każde z nich uruchomi własne odświeżenie, co może unieważnić rotujące tokeny odświeżania; rozwiązanie to jest omówione w dlaczego jednoczesne błędy 401 wylogowują użytkowników. Ponadto przechowywanie tokenów w localStorage naraża je na wszelkie błędy typu XSS, dlatego warto rozważyć użycie plików cookie typu httpOnly.
Błąd 2: brak zwracania wartości z interceptora
Interceptory tworzą łańcuch, a każdy ogniwо musi przekazać swoją wartość następnemu. Te dwa zapomniały o tym:
// ❌ 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!
});
Skutek ten jest często opisywany jako zawieszenie żądania, ale w aktualnych wersjach Axios błąd jest zazwyczaj bardziej bezpośredni i równie mylący. Interceptor odpowiedzi, który nic nie zwraca, rozwiązuje obietnicę wywołującego kodem undefined, więc const { data } = await api.get('/user') wywołuje błąd dekonstrukcji daleki od jego rzeczywistej przyczyny. Interceptor żądania, który nic nie zwraca, pozostawia Axios bez konfiguracji, w wyniku czego żądanie zawodzi jeszcze przed wysłaniem. W obu przypadkach błąd pojawia się w innym miejscu niż tam, gdzie został napisaany.
Zawsze zwracaj to, co otrzymałeś, i wyraźnie odrzuć żądanie w obsłudze błędów, aby kod catch wywołującego nadal mógł zostać wykonywany:
// ✅ 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);
}
);
Analogia do Express pozostaje aktualna: zapomnienie o next() zatrzymuje żądanie w Express, a zapomnienie o return przerywa łańcuch interceptorów.
Błąd 3: ręczne dodawanie nagłówków autoryzacji
Kopiowanie nagłówka Authorization do każdego wywołania traktuje kwestię wspólną dla wielu funkcji jak problem lokalny. Prędzej czy później jakieś wywołanie pominie go, sformatuje inaczej lub wyśle Bearer null. Poniższy fragment pokazuje również, jak łatwo popełnić błąd: w wywołaniu post obiekt nagłówka jest przekazywany jako ciało żądania, a dane jako konfiguracja, ponieważ Axios oczekuje 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}` } });
Interceptor żądań usuwa całą klasę błędów poprzez dodanie nagłówka w jednym miejscu, ale tylko wtedy, gdy istnieje 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;
});
Jeśli używasz TanStack Query, jego bufory oferują również globalne hooki do obsługi efektów ubocznych. Funkcja QueryCache onError może przekierować żądanie przy kodzie błędu 401 dla dowolnego zapytania, a funkcja MutationCache onError może wyświetlić komunikat o błędzie dla każdej nieudanej mutacji:
// ✅ 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);
}
})
});
Zasada leżąca u podstaw polega na tym, że uwierzytelnianie to kwestia warstwy transportowej, a nie komponentu, dlatego powinno być obsługiwane na warstwie transportowej.
Ładowanie danych: loaderzy React Router i cache’y TanStack Query
Błąd 4: wykonywanie żądań w useEffect zamiast użyć loadera trasy
Znany wzorzec to: montowanie komponentu, uruchomienie efektu, wyświetlenie wskaźnika ładowania, otrzymanie danych i renderowanie:
// ❌ 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} />;
}
Spowodowuje to powstawanie „wodospadów żądań”: router renderuje komponent, dopiero wtedy efekt inicjuje żądanie, a dopiero po jego powrocie aktualizuje się stan. Przewijanie się w tę i z powrotem powoduje ponowne pobieranie danych za każdym razem. Starsze wersje Reacta ostrzegały również przed ustawianiem stanu po odmontowaniu komponentu; React 18 usunął to ostrzeżenie, ale podstawowy konflikt pomiędzy wolną a szybszą odpowiedzią nadal istnieje.
API dane React Router (dostępne za pomocą createBrowserRouter od wersji v6.4) umożliwiają routowi określenie loadera, który pobiera dane przed renderowaniem elementu routu, oraz errorElementu, który jest renderowany, jeśli loader wywoła błąd:
// ✅ 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} />;
}
Gdy renderowana jest Dashboard, useLoaderData() już posiada dane, więc nie ma konieczności utrzymywania stanu ładowania na poziomie komponentu ani specjalnych kodów pomocniczych. Warto znać dwa szczegóły. Samo nawigowanie czeka na ładowarkę, dlatego nadal potrzebny jest globalny wskaźnik oczekiwania sterowany przez useNavigation. Ponadto, ponieważ Axios domyślnie odrzuca odpowiedzi innych niż 2xx, warunek response.status !== 200 w tej ładowarce nigdy nie wykrywa stanu błędu; odrzucenie przenosi się automatycznie do errorElement.
Dla danych drugorzędnych, które nie powinny blokować strony, ładowarka może poczekać na krytyczną prośbę i przekazać wolniejszą jako nierozwiązane obietnicy za pomocą 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)
});
}
Strona jest renderowana z podsumowaniem, podczas gdy dane analityczne są dostarczane, zazwyczaj za pomocą Await i Suspense. W React Router v7 funkcja defer nie jest już wymagana, a loaderzy mogą bezpośrednio zwracać obietnice, dlatego sprawdź dokumentację dotyczącą wersji, którą używasz.
Błąd 5: ręcznie pisane thunks dla każdej żądania
Redux Thunk stanowił duży krok naprzód w momencie swojego pojawienia się, ale pisanie takiego thunka dla każdej wywołanej funkcji API oznacza konieczność utrzymywania stałych typów działań, generatorów działań, przypadków reduktora oraz flag stanu dla zadań, które nowoczesne narzędzia mogą obsłużyć deklaratywnie:
// ❌ 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...
Ta struktura była rozsądna, gdy nie istniało nic lepszego. Dla standardowych operacji CRUD jest ona obecnie niepotrzebna.
RTK Query i TanStack Query oferują zaawansowane funkcje takie jak cache, eliminacja duplikatów zapytań, ponowne pobieranie danych w tle oraz stan ładowania od razu po uruchomieniu. Za pomocą RTK Query opisuje się podstawowe zapytanie, w tym hook prepareHeaders do dołączania tokena, oraz deklaruje się punkty końcowe; dla użytkownika generowane są automatycznie hooki takie jak useGetUserQuery i 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;
Za tymi haczkami RTK Query przechowuje w pamięci tymczasowej odpowiedzi dla każdego punktu końcowego i argumentu, liczy, ile zainstalowanych komponentów korzysta z każdej pozycji, i usuwa te, których nikt już nie używa. Należy zauważyć, że nie normalizuje on entytetów w taki sam sposób jak ręcznie stworzony adapter entytetów; wyniki z pamięci tymczasowej są przechowywane w takiej formie, w jakiej zostały zwrócone. W obu przypadkach to ty piszesz logikę biznesową, a biblioteka zajmuje się resztą.
Middleware Redux i zabezpieczenia interfejsu użytkownika
Błąd 6: modyfikacja stanu wewnątrz middleware Redux
Customowe middleware Redux stanowią dobry sposób na logowanie, analizę danych i transformację działań. To jednak nie jest odpowiednie miejsce na zmianę stanu. Middleware działa pomiędzy funkcją dispatch a reduktorem, więc modyfikowanie stanu w tym miejscu omija mechanizm śledzenia zmian w Redux, utrudnia debugowanie oraz wprowadza zamieszanie w narzędziach 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);
};
Mutaacja ta również nie powoduje ponownego renderowania żadnych elementów, ponieważ Redux i jego mechanizmy wykrywają zmiany poprzez porównywanie referencji. Zmiana pola w istniejącym obiekcie sprawia, że referencja pozostaje identyczna, więc subskrybenci nie widzą żadnej aktualizacji. Funkcja sprawdzania niezmienności w trybie rozwojowym Redux Toolkit wykryje tego typu mutację, co jest kolejnym powodem, by go używać.
Ogranicz middleware do czytania, logowania, przekształcania lub przekazywania akcji, a każdą zmianę stanu przechowuj w reduktorach. W tym przypadku middleware śledzi akcję i dołącza zapis czasu jako meta, a reduktor wykorzystuje go do dokonania niezmiennej aktualizacji:
// ✅ 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;
}
}
Zwróć uwagę na powiązanie: reducer odczytuje action.meta.addedAt, więc zależy od zainstalowanego middleware’u. Generowanie daty i godziny w funkcji tworzącej akcję (lub w callbacku prepare w Redux Toolkit) pozwala uniknąć tej ukrytej zależności. Przydatny obraz mentalny: middleware to biuro pocztowe, które potrafi czytać koperty i naklejać znaczki, ale nigdy nie otwiera twojego wyciągu bankowego, by zmienić saldо.
Błąd 7: traktowanie granic błędów jako kwestii drugorzędnego znaczenia
Wiele aplikacji ma jedną taką granicę wokół elementu korzeniowego lub w ogóle żadną. Granice błędów działają jak middleware dla drzewa komponentów: łapią błędy renderowania w swoich potomkach i zapobiegają temu, by jeden błąd doprowadził do awarii całej interfejsu. Bez nich błędna odpowiedź API, brakująca właściwość lub niepracujący skrypt od strony trzeciej mogą sprawić, że cała strona zostanie wyświetlona pusta:
// ❌ Bad: No error boundaries
function App() {
return (
<div>
<Header />
<Sidebar />
<Dashboard /> {/* If this crashes, everything dies */}
<Footer />
</div>
);
}
Umieść granice na poziomie układu i poszczególnych elementów, aby awaria widgetu analitycznego nie powodowała utraty możliwości nawigacji. Granica to komponent klasowy, który ustawia stan błędu w metodzie getDerivedStateFromError, zgłasza błąd w metodzie componentDidCatch oraz wyświetla alternatywne rozwiązanie; użycie kilku takich granic umożliwia każdemu regionowi niezależne odzyskanie funkcjonalności:
// ✅ 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>
);
}
Pamiętaj o ich ograniczeniach: granice łapią błędy powstające podczas renderowania, w metodach cyklu życia oraz w konstruktorach poniżej nich, ale nie błędy w obsługiwaczach zdarzeń, w kodzie asynchronicznym takim jak timerzy czy niewyczekiwane obietnice, ani w samej granicy. Te wymagają zwykłych struktur try/catch lub globalnych obsługiwaczy. Jeśli używasz frameworku z komponentami serwerowymi, łącz granice klienckie z plikami obsługi błędów na poziomie tras (na przykład error.js w Next.js App Router), aby łapać błędy serwera dla każdej trasy oraz błędy klienckie dla każdego komponentu.
Wydajność i granice platformy
Błąd 8: intensywne obliczenia w interceptorach
Interceptory są asynchroniczne w sensie obietnic, ale ich kod nadal jest wykonywany na głównej wątku. Kosztowne dekodowanie, transformacja lub parsowanie w tym miejscu blokuje renderowanie:
// ❌ Bad: Heavy work in interceptor
api.interceptors.response.use((response) => {
// This blocks rendering for large payloads
response.data = heavyDecryptAndTransform(response.data);
return response;
});
200 milisekund transformacji to okres 200 milisekund, w którym strona ignoruje dane wejściowe, co na telefonie sprawia wrażenie zamarłej aplikacji. Przenieś intensywne obliczenia do Web Workera lub odłóż wszystko, co nie jest niezbędne. W tym przypadku odpowiedzi są wysyłane do workera, a interceptor czeka na wynik:
// ✅ 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;
});
Tworzenie nowego pracownika przy każdej odpowiedzi wiąże się z kosztami uruchomienia, a funkcja postMessage kopiuje dane przy użyciu strukturalnego klonowania, dlatego przy częstych wywołaniach należy ponownie wykorzystywać pracownika o długim czasie życia oraz rozważyć użycie buforów przenoszalnych. W celu szyfrowania lub kompresji API Web Crypto wewnątrz pracownika lub przeniesienie obliczeń na serwer zapewniają responsywność interfejsu użytkownika. Aby dowiedzieć się więcej na temat tego kompromisu, zapoznaj się z artykułem kiedy przenieść obliczenia CPU do Web Workera.
Błąd 9: traktowanie middleware w Next.js jak kodu aplikacji
Middleware w Next.js tradycyjnie działa w środowisku Edge Runtime, które jest izolacją V8, a nie pełnym Node.js. Nie ma on dostępu do kontekstu React ani do stanu klienta, a także nie dostępna jest tam zwykła baza połączeń do bazy danych. Mimo to często spotyka się w pliku middleware.ts skomplikowaną weryfikację, zapytania do bazy danych lub logikę opartą na stanie:
// ❌ 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();
}
Niech zajmuje się jedynie prostymi zadaniami na poziomie żądania: przepisywaniem treści, tanimi sprawdzaniami obecności plików cookie, grupowaniem według strategii A/B, filtrowaniem botów oraz routowaniem opartym na lokalizacji geograficznej. Ten przykład kieruje nieautoryzowanych odwiedzających z dala od /dashboard, przekazuje nagłówek eksperymentu do aplikacji i wykorzystuje matcher, aby działać tylko na ścieżkach, które tego wymagają:
// ✅ 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*']
};
Rzeczywista weryfikacja sesji powinna odbywać się w obsługiwaczach tras lub komponentach serwerowych z pełnym dostępem do serwera; middleware pełni rolę bramkarza, a nie biura administracyjnego. Wsparcie w czasie wykonywania zmienia się: najnowsze wersje Next.js dodawały opcję środowiska Node.js dla middleware i zmieniły konwencje plików, dlatego należy sprawdzić aktualne zachowanie w dokumentacji Next.js dla swojej wersji.
Błąd 10: nadmierna konstrukcja małej aplikacji
Nie każdy projekt wymaga zestawu interceptorów, niestandardowego middleware Redux oraz workerów. Łatwo znaleźć strony portfolio składające się z trzech stron z złożonymi łańcuchami interceptorów, ograniczeniami dotyczącymi tekstu statycznego oraz pełną warstwą zapytań tylko dla jednego formularza kontaktowego:
// ❌ 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
}
}
});
To jest kult ładunku, a nie architektura: każda warstwa dodaje obciążenie poznawcze, wagę pakietu oraz pośrednictwo podczas debugowania. Aplikacja z kilkoma trasami i jednym żądaniem nie potrzebuje strategii cacheowania. Konfiguracja jest również wewnętrznie niespójna: przy użyciu staleTime: Infinity dane nigdy nie są uznawane za przestarzałe, więc flagi focus i reconnect refetch nie działają. Ponadto cacheTime zostało przemianowane na gcTime w TanStack Query v5.
Zwiększaj wydajność maszyny wraz z aplikacją:
- Strony statyczne lub proste: wywołuj
fetchbezpośrednio, bez interceptorów. - Małe aplikacje z autoryzacją: jeden interceptor żądań dla tokenów i jeden interceptor odpowiedzi dla błędów 401.
- Aplikacje średniej wielkości z routowaniem: dodaj loaderów routera oraz granice błędów na poziomie układu.
- Zaawansowane aplikacje z złożonym stanem: wykorzystaj RTK Query lub TanStack Query oraz dostosowany middleware.
Wiedza o tym, kiedy nie należy dodawać abstrakcji, jest częścią tej pracy.
Paradigma middleware w aplikacjach React
Middleware to nie tylko koncepcja z obszaru backendu. W React stanowi warstwę, która pozwala komponentom skupić się na renderowaniu, podczas gdy wspólne problemy są rozwiązywane tylko raz.
- Umieść logikę autoryzacji i przekładania błędów w warstwie transportu, na pojedynczej instancji klienta, i zawsze zwracaj wyniki lub odrzucaj je z pomocą interceptorów.
- Ładuj dane tras w specjalnych loaderach lub w pamięci cache z zapytań, zamiast w efektach komponentów.
- Zachowaj tylko efekty uboczne middleware Redux i pozwól reducerom zarządzać zmianami stanu.
- Używaj kilku granic błędów i pamiętaj, czego one nie mogą przechwycić.
- Unikaj wykonywania intensywnych operacji na głównej wątku i utrzymuj middleware typu edge w prostocie.
- Dodawaj każdą warstwę tylko wtedy, gdy rozmiar aplikacji na to pozwala.
Literatura pokrewna
- Redux Toolkit od First Slice do danych asynchronicznych: model mentalny: Dowiedz się, jak sklepa Redux Toolkit, komponenty slices, reduktory oparte na Immer, selektory oraz createAsyncThunk współpracują ze sobą, korzystając z licznika, koszyka zakupowego i listy produktów.
- Dlaczego WebSockets zatrzymują się w ścieżkach API Next.js i jak to naprawia zaawansowany serwer: Dowiedz się, dlaczego serwer ws wewnątrz ścieżki pages/api nigdy nie kończy procesu nawiązywania połączenia, oraz jak samodzielnie obsłużyć zdarzenie upgrade za pomocą dostosowanego serwera Next.js.