Goal
Add memoized selectors with createSelector, show application stats on /saved, review RTK patterns for HireBoard, and finish the matching Taskly selectors milestone before the Final Assessment.
1. Why createSelector?
A selector that returns a new array/object every time can force extra React re-renders:
// Recreates an array on every store update → noisy re-renders
const list = useSelector((state) => Object.values(state.applications.items));
createSelector memoizes the result: it only recomputes when inputs change.
2. Add selectors to applicationsSlice.js
Update the import at the top:
import { createSlice, createSelector } from "@reduxjs/toolkit";
Add at the bottom of the file:
const selectApplicationsState = (state) => state.applications.items;
export const selectSavedEntries = createSelector(
[selectApplicationsState],
(items) => Object.values(items),
);
export const selectSavedCount = createSelector(
[selectSavedEntries],
(entries) => entries.length,
);
export const selectStatusCounts = createSelector(
[selectSavedEntries],
(entries) => {
const counts = {
saved: 0,
applied: 0,
interviewing: 0,
offer: 0,
rejected: 0,
};
for (const entry of entries) {
counts[entry.status] = (counts[entry.status] || 0) + 1;
}
return counts;
},
);
Chunks explained
| Chunk | What it does |
|---|---|
createSelector([input], fn) |
Memoized derived data |
selectStatusCounts |
Stats object for the Saved page |
3. StatsBar.jsx
import { useSelector } from "react-redux";
import { selectStatusCounts } from "../store/applicationsSlice";
function StatsBar() {
const counts = useSelector(selectStatusCounts);
const chips = Object.entries(counts);
return (
<div className="mb-8 grid grid-cols-2 gap-3 sm:grid-cols-5">
{chips.map(([status, count]) => (
<div
key={status}
className="rounded-lg border border-slate-200 bg-card px-3 py-3 text-center dark:border-slate-700 dark:bg-slate-900"
>
<p className="text-2xl font-semibold text-brand-600">{count}</p>
<p className="text-xs font-medium uppercase tracking-wide text-muted dark:text-slate-400">
{status}
</p>
</div>
))}
</div>
);
}
export default StatsBar;
4. Use selectors in SavedJobs.jsx
import { useSelector } from "react-redux";
import { Link } from "@tanstack/react-router";
import { selectSavedEntries } from "../store/applicationsSlice";
import JobCard from "./JobCard";
import StatsBar from "./StatsBar";
function SavedJobs() {
const entries = useSelector(selectSavedEntries);
if (entries.length === 0) {
return (
<section>
<h2 className="mb-2 text-2xl font-semibold">Saved applications</h2>
<p className="text-muted dark:text-slate-400">
No saved jobs yet.{" "}
<Link to="/jobs" className="font-medium text-brand-600 underline">
Browse jobs
</Link>{" "}
and click Save job.
</p>
</section>
);
}
return (
<section>
<div className="mb-6">
<h2 className="text-2xl font-semibold">Saved applications</h2>
<p className="text-sm text-muted dark:text-slate-400">
{entries.length} saved · update status as you progress
</p>
</div>
<StatsBar />
<div className="grid gap-4 sm:grid-cols-2">
{entries.map(({ job }) => (
<JobCard key={job.id} job={job} showStatus />
))}
</div>
</section>
);
}
export default SavedJobs;
5. Final project structure
hireboard/
├── index.html
├── vite.config.js
├── package.json
└── src/
├── main.jsx ← Provider
├── App.jsx ← RouterProvider
├── style.css
├── routeTree.gen.ts ← generated
├── utils/
│ └── cookies.js ← token helpers
├── store/
│ ├── index.js
│ ├── uiSlice.js
│ ├── authSlice.js ← user: localStorage, token: cookie
│ ├── applicationsSlice.js
├── apis/
│ ├── loginApi.js ← login mutation
│ └── jobsApi.js ← jobs query
├── components/
│ ├── Navbar.jsx
│ ├── ThemeToggle.jsx
│ ├── LoginForm.jsx
│ ├── JobsList.jsx
│ ├── JobCard.jsx
│ ├── SavedJobs.jsx
│ └── StatsBar.jsx
└── routes/
├── __root.jsx
├── index.jsx
├── login.jsx
├── _authenticated.jsx ← protected layout (beforeLoad)
└── _authenticated/
├── jobs.jsx
└── saved.jsx
6. End-to-end checklist
/redirects to/loginor/jobsbased on the token cookie.- Sign in with
emilys/emilyspass→ Network showsPOST /auth/login. - Cookie
hireboard-tokenset;localStoragehashireboard-user. - Land on
/jobs→ Network showsGET /products?limit=12. - Save jobs → open
/saved→ change status → refresh keeps data. - Open
/jobsin a private window → redirected to/login. - Log out → cookie + user cleared →
/login. - Theme toggle persists in
localStorage.
7. Mostly used topics (cheat sheet)
| Topic | What it is | When you use it | HireBoard where |
|---|---|---|---|
configureStore |
Builds the Redux store with DevTools and middleware | When creating the store | store/index.js |
Provider |
Makes the store available to React components | At app root | main.jsx |
createSlice |
Bundles reducer + actions + action types for one feature | For each domain area | slices |
useSelector |
Reads state from the store in a component | Whenever UI needs store data | components |
useDispatch |
Sends actions into the store | For user events and state changes | components |
createApi |
Defines RTK Query endpoints and cache behavior | For shared HTTP APIs | loginApi.js, jobsApi.js |
fetchBaseQuery |
Simplified fetch wrapper with base URL and headers | When building API endpoints | loginApi.js, jobsApi.js |
builder.query |
Creates a cached read endpoint | For GET-style requests | getJobs |
builder.mutation |
Creates a write endpoint | For POST/PUT/DELETE | login |
prepareHeaders |
Adds headers to outgoing requests | For auth or custom headers | fetchBaseQuery |
createSelector |
Memoizes derived state selectors | For derived lists/counts | applications selectors |
localStorage |
Browser persistence for durable app state | For theme/user data | auth / ui / applications |
document.cookie |
Browser persistence for auth token | For auth session tokens | utils/cookies.js |
| TanStack Router | Client-side route system with layout routes | For multi-page navigation | src/routes/* |
createSlice |
Session, theme, saved apps | slices | |
useSelector / useDispatch |
Connect UI | components | |
RTK Query createApi |
All HTTP | loginApi.js, jobsApi.js |
|
builder.mutation |
Login | login |
|
builder.query + transformResponse |
Live jobs list | getJobs |
|
prepareHeaders |
Attach Bearer token | fetchBaseQuery |
|
| Cookie helpers | Persist token | utils/cookies.js |
|
localStorage |
Persist user + theme + saves | auth / ui / applications | |
| TanStack Router | Real URLs | src/routes/* |
|
beforeLoad + redirect |
Protected routes | _authenticated.jsx |
|
createSelector |
Derived stats | applications selectors | |
| Redux DevTools | Debug | browser extension |
Patterns
UI event
→ mutation/query or dispatch(slice action)
→ cookie / localStorage / API cache update
→ selector → re-render
- Local form inputs →
useState - Session → slice + cookie (token) + localStorage (user)
- Remote data → RTK Query
- URLs / guards → TanStack Router
- Derived lists / counts →
createSelector
Stretch ideas
- Call
GET /auth/mewith the Bearer token after refresh to revalidate the session. - Add
search.redirecthandling on the login form after a guarded redirect. - Replace product mapping with your own backend
/jobsendpoint. - Use httpOnly cookies from a real server instead of
document.cookie.
Assessment: Taskly : Selectors
Add memoized selectors with createSelector:
- list of pinned entries
- pin count
- counts by priority (
low/medium/high)
Show a small stats bar on /pinned.
Done when: /pinned shows priority counts that update when you change priorities.
HireBoard teaching complete
You finished the guided HireBoard build. Your graded work is Taskly.
Official docs: Redux Toolkit · TanStack Router