ADR-005: RTK Query for Server State Management

Status: Accepted

Context

The frontend needs to fetch, cache, and mutate server data with:

  • Automatic cache invalidation when mutations succeed
  • Token refresh on 401 without manual retry logic in every component
  • Typed request / response shapes
  • Consistent loading and error states across the app

Options considered:

LibraryRedux integrationAuto-refreshTyped queriesBundle size
RTK QueryNative (RTK)Via base query~10 KB (already using RTK)
React QuerySeparate storeManual~13 KB
SWRNoneManualPartial~4 KB
Raw fetchManualManualManual0 KB

The project already uses Redux Toolkit for client-side state (auth token, theme, login form). Adding RTK Query means zero extra dependencies — the cache, actions, and middleware live inside the existing Redux store.

Decision

Use RTK Query (bundled with @reduxjs/toolkit) for all server data fetching and mutations.

Architecture:

  • A single baseApi (frontend/redux/api/baseApi.ts) is created with createApi. All feature APIs inject their endpoints with baseApi.injectEndpoints().
  • The baseQuery wraps fetchBaseQuery with a re-auth interceptor: on any 401 response it fires POST /api/auth/refresh, stores the new token in Redux, and retries the original request — transparently, in every query and mutation.
  • A concurrency guard prevents refresh storms: if multiple requests fail with 401 simultaneously, only the first triggers a refresh; the rest wait on a shared refreshPromise.
baseApi
  ├── authApi   (login, logout, setPassword, changePassword, refreshToken)
  └── integrationsApi (integrations list + toggle)

Cache tag invalidation follows RTK Query conventions — mutations call invalidatesTags to trigger refetches of affected queries.

The baseApi.reducerPath ("api") is added to the Redux store alongside the feature slices.

Consequences

Positive

  • Token refresh is centralised in one place (baseQueryWithReauth) — components never need catch (e) { if (401) refresh() } logic
  • Typed endpoints: each endpoint declares its Result and Arg types; RTK Query generates hooks (useGetUsersQuery, useLoginMutation) with full TypeScript inference
  • Cache is part of the Redux store — Redux DevTools shows all cached data and pending requests
  • No extra dependencies — RTK Query ships inside @reduxjs/toolkit
  • Consistent loading/error shapes: { isLoading, isError, data, error } in every component

Negative / Risks

  • RTK Query has a larger learning curve than a plain useFetch hook; the createApi / injectEndpoints / tag invalidation model takes time to internalise
  • The re-auth interceptor silently retries on 401 — a malformed token that consistently returns 401 would cause an infinite refresh loop (mitigated: clearToken() is dispatched on refresh failure, redirecting to login)
  • RTK Query caching is in-memory; the cache is cleared on page reload (server state is refetched; client state like the auth token is recovered from localStorage)