Redux State Management Analysis
Analysis of how the arda-frontend-app uses Redux Toolkit, React-Redux, and Redux Persist to manage application state. This is an explanation-type document intended to inform the List View Component design by documenting the existing state management patterns.
Libraries
Section titled “Libraries”| Library | Version | Role |
|---|---|---|
@reduxjs/toolkit | ^2.11.2 | Core state management (slices, thunks, configureStore) |
react-redux | ^9.2.0 | React bindings (Provider, hooks) |
redux-persist | ^6.0.0 | Selective localStorage persistence and rehydration |
The application does not use Redux Saga, RTK Query, or the legacy redux package directly.
Store Configuration
Section titled “Store Configuration”The store is configured in src/store/store.ts using configureStore from Redux Toolkit.
configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: { ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], }, }) .concat(loggerMiddleware) // dev-only action logger .concat(tokenRefreshMiddleware), // auto-refreshes expiring tokens devTools: process.env.NODE_ENV === 'development',});Key decisions:
- The serializable check ignores Redux Persist lifecycle actions.
- Redux DevTools are enabled only in development.
RootStateis derived fromrootReducer(notstore.getState) to avoid circular type references.AppDispatchis exported for typed thunk dispatch.
Slices
Section titled “Slices”The root reducer (src/store/rootReducer.ts) combines six slices, each created with createSlice.
| Slice | File | Purpose | Persisted Fields |
|---|---|---|---|
auth | slices/authSlice.ts | User identity, tokens, JWT payload, session state | tokens, user, userContext, jwtPayload |
ui | slices/uiSlice.ts | Sidebar visibility, theme, preferences | sidebarVisibility |
items | slices/itemsSlice.ts | Item listings, editing, drafts, pagination, column visibility | columnVisibility, drafts, activeTab |
scan | slices/scanSlice.ts | QR scanning state, filters, selection | columnVisibility, selectedFilters |
orderQueue | slices/orderQueueSlice.ts | Order queue counts, groups, selection | None (ephemeral) |
receiving | slices/receivingSlice.ts | Receiving tracking state | None (ephemeral) |
Each persisted slice uses a dedicated persistReducer() call with an explicit whitelist. Ephemeral UI state (loading, error, isRefreshing) is blacklisted so it resets on reload.
Persistence and Rehydration
Section titled “Persistence and Rehydration”Persistence is configured per-slice in src/store/rootReducer.ts with redux-persist.
- Storage backend: Conditional
localStorageimport (SSR-safe; falls back to no-op on the server). - Granularity: Each slice has its own
persistConfigwith a unique key (e.g.,persist:auth,persist:items), enabling fine-grained control over what survives page reload. - Rehydration gate: The
ReduxProvidercomponent (src/store/ReduxProvider.tsx) wraps the app in<PersistGate loading={<Loader />}>, blocking render until rehydration completes. - Legacy migration:
AuthInit(src/store/components/AuthInit.tsx) handles one-time migration from pre-ReduxlocalStorageentries and re-validates tokens on hydration.
Async Thunks
Section titled “Async Thunks”All asynchronous operations use createAsyncThunk. There is no use of sagas or raw middleware-based async flows.
Auth Thunks (src/store/thunks/authThunks.ts)
Section titled “Auth Thunks (src/store/thunks/authThunks.ts)”| Thunk | Purpose |
|---|---|
signInThunk | Cognito login; handles NEW_PASSWORD_REQUIRED challenge |
respondToNewPasswordChallengeThunk | Completes forced password change |
signOutThunk | Global Cognito sign-out and local token clearing |
refreshTokensThunk | Proactive token refresh; distinguishes transient vs. permanent failures |
checkAuthThunk | Local JWT validation (no network call) |
forgotPasswordThunk | Initiates password reset flow |
confirmNewPasswordThunk | Completes password reset |
changePasswordThunk | Authenticated password change |
Each thunk maps Cognito SDK errors to user-friendly messages and supports a mock-mode bypass (NEXT_PUBLIC_MOCK_MODE=true).
Order Queue Thunk (src/store/thunks/orderQueueThunks.ts)
Section titled “Order Queue Thunk (src/store/thunks/orderQueueThunks.ts)”fetchOrderQueueCountThunk fetches counts from three Kanban endpoints in parallel.
Middleware
Section titled “Middleware”Two custom middleware functions are registered after the default RTK middleware.
Logger (src/store/middleware/logger.ts)
Section titled “Logger (src/store/middleware/logger.ts)”Development-only middleware that logs every action and the resulting state to the console using console.group().
Token Refresh (src/store/middleware/tokenRefreshMiddleware.ts)
Section titled “Token Refresh (src/store/middleware/tokenRefreshMiddleware.ts)”Runs after every action (post-next(action)) and checks whether the access token is within 5 minutes of expiry. If so, it dispatches refreshTokensThunk. A 60-second cooldown prevents retry storms after transient failures, and permanent session expiry stops all further refresh attempts.
Selectors
Section titled “Selectors”Selectors are organized into per-slice files under src/store/selectors/.
- Simple selectors: Direct state lookups (e.g.,
selectUser,selectTokens,selectActiveTab). - Memoized selectors: Parameterized lookups using
createSelector(e.g.,selectItemById,selectColumnVisibilityForTab,selectDraft). These return stable empty-object/empty-array fallbacks to prevent unnecessary re-renders.
Approximately 70+ selectors are defined across six selector files.
Typed Hooks
Section titled “Typed Hooks”src/store/hooks.ts exports typed wrappers that all components use instead of the bare react-redux hooks:
export const useAppDispatch = () => useDispatch<AppDispatch>();export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;Domain Hooks
Section titled “Domain Hooks”| Hook | File | Role |
|---|---|---|
useAuth | hooks/useAuth.ts | High-level auth API wrapping thunks (signIn, signOut, ensureValidTokens, etc.) |
useOrderQueue | hooks/useOrderQueue.ts | Auto-fetches order queue count on mount when user/token changes |
useSidebarVisibility | hooks/useSidebarVisibility.ts | Dispatches sidebar toggle actions |
useJWT | hooks/useJWT.ts | JWT-specific convenience selectors |
Components never call useDispatch or useSelector directly; they use useAppDispatch, useAppSelector, or a domain hook.
Provider Hierarchy
Section titled “Provider Hierarchy”The provider nesting order is defined in src/app/layout.tsx:
MSWInit (gates rendering until MSW worker starts in mock mode) ReduxProvider PersistGate (loading={<Loader />}) AuthInit (rehydration + legacy migration) OrderQueueInit (auto-fetch on mount) AppContent (routes and pages)ReduxProvider (src/store/ReduxProvider.tsx) owns both the Redux <Provider> and the <PersistGate>.
Component Integration Patterns
Section titled “Component Integration Patterns”Components follow a consistent pattern:
- Select state with a typed selector:
const user = useAppSelector(selectUser); - Dispatch actions with the typed dispatch:
const dispatch = useAppDispatch(); - Use domain hooks for complex flows:
const { signIn, loading, error } = useAuth();
Key integration points:
- Auth guard: Checks
selectIsAuthenticatedto protect routes. - Sign-in page: Uses
useAuth().signIn(). - Sidebar: Dispatches
toggleSidebarItem()viauseSidebarVisibility. - Order queue badge:
useOrderQueue()polls on init. - API calls:
useAuth().ensureValidTokens()is called before fetch operations.
Impact on List View Component Refactor
Section titled “Impact on List View Component Refactor”The List View Component design (Tiers 1–3b) lives in ux-prototype as a state-management-agnostic component library. It communicates entirely through props and callbacks (rowData, onSelectionChanged, paginationData, columnVisibility, etc.). Redux is an application-level concern in arda-frontend-app. The tiered architecture already assumes the consuming application owns the state, which is exactly what Redux does. The component design does not need to change.
Clean Integration Points (No Friction)
Section titled “Clean Integration Points (No Friction)”- Row data, loading, error — The
itemsslice holdsitems,loading,error. These map directly toDataGridRuntimeConfig.rowData,.loading,.error. A selector feeds each prop. - Pagination — The
itemsslice haspaginationstate. The component takesPaginationDataplusonNextPage/onPreviousPagecallbacks. Redux dispatch goes in the callbacks. - Selection —
selectedItemslives in Redux. The component providesonSelectionChanged(rows). The handler dispatches to the slice. - Search and filter —
searchanddebouncedSearchare in the Reduxitemsslice. The component’shasActiveSearch(Tier 3b) andenableFiltering(Tier 3a) are complementary — external search stays in Redux, grid-level column filtering is internal to AG Grid.
Friction Point 1: Column Visibility Dual Persistence
Section titled “Friction Point 1: Column Visibility Dual Persistence”The component library includes a useColumnPersistence hook that reads and writes column state directly to localStorage using a persistenceKey. Meanwhile, the items and scan Redux slices persist columnVisibility via redux-persist (which also writes to localStorage under persist:items / persist:scan).
When arda-frontend-app adopts the new DataGrid, both mechanisms would write column visibility to localStorage independently, potentially conflicting or drifting.
Resolution: Omit the persistenceKey prop at Tier 4 integration time and drive column visibility entirely through the columnVisibility prop on EntityGridViewProps. The useColumnPersistence hook activates only when persistenceKey is provided, so the two mechanisms cannot conflict. This keeps Redux as the single source of truth, matching the existing application pattern. The useColumnPersistence hook remains available for consumers that do not have their own state management layer.
Friction Point 2: Dirty Tracking vs. Redux drafts
Section titled “Friction Point 2: Dirty Tracking vs. Redux drafts”The design proposes a useDirtyTracking<T>() hook that uses pure React state (local useState/useRef). The items slice in arda-frontend-app already persists drafts via Redux Persist, surviving page reloads.
The library hook would lose unsaved drafts on refresh unless the consuming application wires it to Redux. At Tier 4 integration time, each domain grid needs a Redux-backed alternative to the library hook.
Resolution: Each Tier 4 domain grid writes its own thin hook (~12 lines) that maps slice selectors and actions to the dirty tracking interface:
function useItemsDirtyTracking() { const dispatch = useAppDispatch(); const drafts = useAppSelector(selectDrafts);
return { drafts, hasUnsavedChanges: Object.keys(drafts).length > 0, setDraft: (id: string, changes: Partial<Item>) => dispatch(setItemDraft({ id, changes })), discardAllDrafts: () => dispatch(clearAllItemDrafts()), saveAllDrafts: () => { /* dispatch bulk save */ }, };}A generic factory hook was considered and rejected. The per-entity hook is so short that the abstraction overhead (configuration object, generic type threading, mental indirection) exceeds the line savings across the expected 2–4 entity types. Each entity’s dirty tracking may also diverge slightly (Items persist drafts across sessions; a future entity might not), and a concrete hook makes those divergences trivial to express.
Summary
Section titled “Summary”| Concern | Integration Path | Friction |
|---|---|---|
| Row data, loading, error | Selector to prop | None |
| Pagination | Selector to prop, dispatch in callback | None |
| Selection | onSelectionChanged dispatches to slice | None |
| Search and filter | Redux owns external search; AG Grid owns column filters | None |
| Column visibility | Omit persistenceKey; drive via columnVisibility prop from Redux | Low — design already supports this |
| Dirty tracking | Per-entity Redux-backed hook (~12 lines each) replaces library hook | Low — straightforward per-entity wiring |
No changes are required to the Tier 1–3b component library design. Both friction points are resolved at Tier 4 integration time with minimal, per-entity wiring code.
Copyright: © Arda Systems 2025-2026, All rights reserved