Goal

Build ThemeToggle and a Navbar shell with useSelector / useDispatch. Real page links come later with TanStack Router.

1. Mental model

Hook Use it to…
useSelector(fn) Read state (like a subscription)
useDispatch() Write state by dispatching actions

Never mutate state inside a component. Always dispatch(action).

2. ThemeToggle.jsx

Create src/components/ThemeToggle.jsx:

import { useDispatch, useSelector } from "react-redux";
import { toggleTheme } from "../store/uiSlice";

function ThemeToggle() {
  const theme = useSelector((state) => state.ui.theme);
  const dispatch = useDispatch();

  return (
    <button
      type="button"
      onClick={() => dispatch(toggleTheme())}
      className="rounded-md border border-slate-300 px-3 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-100 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-800"
      aria-label="Toggle dark mode"
    >
      {theme === "dark" ? "Light" : "Dark"}
    </button>
  );
}

export default ThemeToggle;

Redux chunks explained

Chunk What it does
useSelector((state) => state.ui.theme) Read current theme
useDispatch() Get the store's dispatch function
dispatch(toggleTheme()) Run the ui reducer and update theme

Tailwind chunks explained

Class Meaning
rounded-md Soft corners
border border-slate-300 Light outline
px-3 py-2 Comfortable click target
transition Smooth hover / theme change
dark:… Styles when .dark is on <html>

3. Navbar.jsx

Create src/components/Navbar.jsx (links stay placeholders until the router lesson):

import ThemeToggle from "./ThemeToggle";

function Navbar() {
  return (
    <header className="sticky top-0 z-10 border-b border-slate-200 bg-card/90 backdrop-blur dark:border-slate-800 dark:bg-slate-900/90">
      <div className="mx-auto flex max-w-5xl items-center justify-between gap-4 px-6 py-3">
        <h1 className="text-lg font-semibold text-brand-600">HireBoard</h1>
        <ThemeToggle />
      </div>
    </header>
  );
}

export default Navbar;

Tailwind chunks explained

Class Meaning
sticky top-0 z-10 Nav stays visible while scrolling
bg-card/90 backdrop-blur Slightly see-through bar
flex … justify-between Brand left, toggle right

4. Wire App.jsx

import { useEffect } from "react";
import { useSelector } from "react-redux";
import Navbar from "./components/Navbar";

function App() {
  const theme = useSelector((state) => state.ui.theme);

  useEffect(() => {
    document.documentElement.classList.toggle("dark", theme === "dark");
  }, [theme]);

  return (
    <div className="min-h-screen bg-surface font-sans text-ink antialiased dark:bg-slate-950 dark:text-slate-100">
      <Navbar />
      <main className="mx-auto max-w-5xl px-6 py-10">
        <h2 className="mb-2 text-2xl font-semibold">HireBoard shell</h2>
        <p className="text-muted dark:text-slate-400">
          Theme works. Auth and TanStack Router come next.
        </p>
      </main>
    </div>
  );
}

export default App;

Chunks explained

Chunk What it does
useSelector for theme Re-render when theme changes
useEffect on theme Syncs the .dark class on <html>

5. Selector tip (avoid this)

// Bad: returns a new object every time → extra re-renders
const ui = useSelector((state) => ({ theme: state.ui.theme }));

Prefer selecting primitives, or use createSelector later for derived objects.

Check

  • Clicking Dark / Light flips the page colors and updates Redux DevTools.
  • Refresh keeps the theme (from localStorage in the slice).

Assessment: Taskly : Hooks and theme

Build ThemeToggle and a Taskly Navbar shell with useSelector / useDispatch. Dark mode must follow a .dark class on <html>.

Done when: theme toggle works and survives refresh in Taskly.

Next: add an auth slice , user in localStorage, token in cookies.