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
localStoragein 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.