Skip to content

Routing & State Management

Router

react-router-dom v6.26.2, BrowserRouter, declarative <Routes> in src/App.tsx.

Routes

Path Auth Layout Component Purpose
/ public (none) <Navigate to="/login" replace /> redirect to login
/login public (none) <Login /> login form
/accounts protected AppLayout <Accounts /> customer-account list/management
/personnel protected AppLayout <Personnel /> internal staff list/management
/prompts protected AppLayout <Prompts /> AI prompt management (route exists; sidebar nav commented out)
/affiliate-marketing protected AppLayout <AffiliateMarketing /> affiliate marketing config
/customer-service protected AppLayout <CustomerService /> customer-support tooling
/my-profile protected AppLayout <MyProfile /> logged-in user's profile
* (any) (none) <NotFound /> 404

6 user-visible routes (counting /prompts since it's mounted, even if hidden from nav). The <Sidebar> exposes 5 (Accounts, Personnel, Affiliate Marketing, Customer Service, My Profile).

Protection pattern

<Route path="/accounts" element={
  <ProtectedRoute>
    <AppLayout><Accounts /></AppLayout>
  </ProtectedRoute>
} />

<ProtectedRoute> calls useAuth(), runs checkAuth() in useEffect, shows a skeleton while loading, redirects to /login if unauthenticated. The skeleton is plain Tailwind divs — no library spinner.

Deep linking

Routes work on refresh because: - <ProtectedRoute> re-runs checkAuth() on mount, which validates localStorage["auth_token"] against /me - vite.config.ts enables SPA fallback by default

A direct visit to /accounts without a token redirects through /login and does not preserve the intended URL — after login, the user is hardcoded to /accounts (per AuthContext.tsx login(): navigate("/accounts", { replace: true })). For an internal tool this is fine; if links from external systems become important, add an ?next= query param.

Code splitting

No React.lazy() / Suspense boundaries. Every page is statically imported in App.tsx. This means one chunk for the whole app (verify with npm run build). For a 6-page app with no heavy dependencies per route, this is fine — total bundle is probably ~300 KB gzipped. As more pages get added, route-based code splitting would be the first optimisation.

Error boundaries

No React error boundaries are mounted. An uncaught render error in any page produces a blank white screen. Adding an ErrorBoundary around <AppLayout> (or at the route level) would degrade gracefully.

State management

Global state

There is no Redux / Zustand / MobX / Pinia. The only global state is:

  1. AuthContext (src/context/AuthContext.tsx) — user, token, isAuthenticated, isLoading, login, logout, checkAuth
  2. @tanstack/react-query QueryClient mounted at the root — used for server-state caching but at audit time useQuery / useMutation adoption appears partial (verify by grep -r "useQuery\|useMutation" src/)
  3. Shadcn toast registry — internal to use-toast.ts

Component-local state

useState for forms, lists, filters. No reducer pattern, no state machines.

Persistence

  • localStorage["auth_token"] — the encrypted auth token
  • localStorage["useid"] — current user's id (set after successful login)
  • localStorage["user_email"] — current user's email (set after successful login)

No sessionStorage, no IndexedDB.

There is no multi-tab synchronisation. If the user logs out in one tab, the other tabs still hold a valid useAuth() context until they re-render and checkAuth() notices the missing token. A storage-event listener on the auth_token key would fix this in ~5 lines.

Caching

  • React Query is mounted but lightly used. The cache TTL is the library default (staleTime: 0), so most queries refetch on focus / mount. This is fine for an internal tool with low traffic.
  • No HTTP-level caching is configured in api.ts (no Cache-Control honoured beyond browser defaults).

Data flow per request

component
  ↓ calls api.getX(...)         (in src/services/api.ts)
  ↓ → request(url, "GET", ...)
  ↓   → createAuthHeaders()    (reads localStorage.auth_token)
  ↓   → fetch(`${ENV.API_URL}${url}`, options)
  ↓   → handleResponse()       (throws on !ok with parsed message)
  ↓ try/catch in api.ts        (toast on error, then re-throw)
  ↓ try/catch in component     (sets loading state false, etc.)
component renders

Errors trigger an automatic toast from api.ts and re-throw to the component. This double-handling means the component should not also toast — but several pages do. Verify and reduce duplication.

Recommendations

  1. Add an <ErrorBoundary> around the protected layout.
  2. Add ?next= preservation to the login flow if external deep links matter.
  3. Add a storage-event listener for multi-tab logout synchronisation.
  4. Standardise on React Query: every server fetch should go through useQuery / useMutation. The current half-state (some via React Query, some via raw fetch + useState) produces inconsistent loading states.
  5. Consider route-based code splitting before the app grows past ~15 routes.