LWS Job Portal deliberately avoids heavyweight state-management libraries. Instead it splits state into two clear layers: a tiny hand-rolled pub-sub store that owns authentication data, and TanStack React Query that owns every piece of server-fetched data. This separation means components only re-render when the slice of state they actually depend on changes, and the query cache handles all the complexity of loading, error, and stale-while-revalidate logic automatically.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/iDevRanjan/lws-ra-b4-assignment-five/llms.txt
Use this file to discover all available pages before exploring further.
Two-Layer State Architecture
Global Auth State
A vanilla JavaScript pub-sub object (
src/store/index.js) holds the single authData key. React reads it through useSyncExternalStore — no Context Provider, no Redux, no Zustand.Server State
TanStack React Query owns all remote data. Every API resource has a typed
queryOptions() factory in queryOptions.js so query keys and fetch functions are defined once and reused everywhere.The Custom Pub-Sub Store
How it works
createStore in src/store/index.js is a factory that returns three functions — getState, setState, and subscribe — backed by a plain object and a Set of listener callbacks. Every call to setState that produces a changed value fans out to all registered listeners synchronously.
setState uses Object.is to compare the previous and next values. If nothing changed, listeners are not called — preventing unnecessary re-renders.Key design decisions
Selector-function key access
Selector-function key access
Rather than accepting a raw string key,
setState requires a selector function (keys) => keys.authData. This gives you autocomplete in IDEs and prevents typos from silently writing to a nonexistent key.Functional updates
Functional updates
nextStateOrFn can be a function that receives the current value: setState((k) => k.authData, (prev) => ({ ...prev, token: newToken })). This mirrors the useState updater pattern React developers already know.Automatic unsubscribe
Automatic unsubscribe
subscribe returns a cleanup function () => listeners.delete(listener). React’s useSyncExternalStore calls this automatically on unmount, so there are no memory leaks.Initial auth state
The store is seeded fromlocalStorage on page load. If no persisted value exists, initialAuthState is used:
role, token, and profile fields when the login action runs.
Auth Actions
src/store/actions/authActions.js exposes two mutations — login and logout — that keep the store, localStorage, and the React Query cache consistent at all times.
login(loginResponseData)
Removes any stale anonymous profile cache entry, persists the JWT payload to
localStorage, then updates the store — triggering a synchronous re-render in every useAuth subscriber.The useAuth Hook
Components never read the store directly. They consume useAuth, which wires useSyncExternalStore to the pub-sub store and co-locates the auth actions so callers get everything in one import.
useSyncExternalStore is a React 18+ built-in that subscribes to an external store safely — handling concurrent-mode tearing and server-side snapshots without any additional boilerplate.
Typical usage in a component:
Filter State: QueryObjectProvider and useQueryObject
The job listing page maintains a rich filter object (search term, job type, experience level, salary range, skills, sort order). This state lives in QueryObjectProvider, which wraps only the <Home /> page — keeping filter state scoped and garbage-collected when the user navigates away.
useQueryObject:
The useDebounce Hook
Search inputs call handleSetQuerySearch on every keystroke. Without debouncing, this would fire a new API request for every character typed. useDebounce wraps any function and delays its execution until the user stops typing:
useRef persists the timeout ID across renders without causing re-renders of its own. The default delay is 300 ms, which is imperceptible to users but reduces API calls by an order of magnitude during fast typing.
TanStack React Query Setup
QueryClient configuration
src/services/queryClient.js creates the singleton QueryClient shared by the entire app:
queryOptions.js — reusable query factories
Every query is defined as a factory function returning a queryOptions() object. This pattern means the query key and fetch function are co-located and can be used identically in both hooks (useQuery) and router loaders (queryClient.ensureQueryData):
Query key conventions
All query keys follow the pattern[QUERY_KEYS.<name>, params] where params is the variable part (slug, filter object, page number, etc.). String constants live in src/utils/constants.js:
mutationOptions.js — reusable mutation configs
Mutations follow the same factory pattern. Side-effects like cache invalidation live inside onSuccess callbacks:
Compound Hooks
Two hooks composeuseAuth and useQuery into single-import utilities for common data needs:
- useProfile
- useApplications
getClientProfileQueryOption which sets enabled: authData.isLoggedin, so the query only fires when the user is authenticated.This architecture intentionally avoids Redux, Zustand, and heavy Context usage. The pub-sub store is a 40-line vanilla JS object. The only React-specific primitive it touches is
useSyncExternalStore — making the store itself framework-agnostic and trivially testable.