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:
AuthContext(src/context/AuthContext.tsx) — user, token, isAuthenticated, isLoading, login, logout, checkAuth@tanstack/react-queryQueryClientmounted at the root — used for server-state caching but at audit timeuseQuery/useMutationadoption appears partial (verify bygrep -r "useQuery\|useMutation" src/)- 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 tokenlocalStorage["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(noCache-Controlhonoured 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¶
- Add an
<ErrorBoundary>around the protected layout. - Add
?next=preservation to the login flow if external deep links matter. - Add a
storage-event listener for multi-tab logout synchronisation. - Standardise on React Query: every server fetch should go through
useQuery/useMutation. The current half-state (some via React Query, some via rawfetch+useState) produces inconsistent loading states. - Consider route-based code splitting before the app grows past ~15 routes.