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

  1. / redirects to /login or /jobs based on the token cookie.
  2. Sign in with emilys / emilyspass → Network shows POST /auth/login.
  3. Cookie hireboard-token set; localStorage has hireboard-user.
  4. Land on /jobs → Network shows GET /products?limit=12.
  5. Save jobs → open /saved → change status → refresh keeps data.
  6. Open /jobs in a private window → redirected to /login.
  7. Log out → cookie + user cleared → /login.
  8. 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 inputsuseState
  • Session → slice + cookie (token) + localStorage (user)
  • Remote data → RTK Query
  • URLs / guards → TanStack Router
  • Derived lists / countscreateSelector

Stretch ideas

  • Call GET /auth/me with the Bearer token after refresh to revalidate the session.
  • Add search.redirect handling on the login form after a guarded redirect.
  • Replace product mapping with your own backend /jobs endpoint.
  • 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