Goal
Add cookie helpers, an auth slice that stores the user in localStorage and the token in a cookie, and a Tailwind LoginForm shell (API call comes next).
1. Create src/utils/cookies.js
export function getCookie(name) {
const match = document.cookie
.split("; ")
.find((row) => row.startsWith(`${name}=`));
if (!match) return null;
return decodeURIComponent(match.split("=").slice(1).join("="));
}
export function setCookie(name, value, days = 7) {
const maxAge = days * 24 * 60 * 60;
document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; path=/; max-age=${maxAge}; SameSite=Lax`;
}
export function deleteCookie(name) {
document.cookie = `${encodeURIComponent(name)}=; path=/; max-age=0; SameSite=Lax`;
}
Chunks explained
| Chunk | What it does |
|---|---|
getCookie |
Reads one cookie by name |
setCookie |
Writes cookie with max-age (expires after days) |
SameSite=Lax |
Reasonable default against cross-site POST CSRF |
deleteCookie |
Expires the cookie immediately |
encodeURIComponent |
Safe for tokens / special characters |
2. Create src/store/authSlice.js
import { createSlice } from "@reduxjs/toolkit";
import { deleteCookie, getCookie, setCookie } from "../utils/cookies";
const TOKEN_KEY = "hireboard-token";
const USER_KEY = "hireboard-user";
function readStoredUser() {
try {
const raw = localStorage.getItem(USER_KEY);
return raw ? JSON.parse(raw) : null;
} catch {
return null;
}
}
const authSlice = createSlice({
name: "auth",
initialState: {
user: readStoredUser(),
token: getCookie(TOKEN_KEY),
},
reducers: {
setCredentials(state, action) {
const payload = action.payload;
state.token = payload.accessToken;
state.user = {
id: payload.id,
username: payload.username,
email: payload.email,
firstName: payload.firstName,
lastName: payload.lastName,
image: payload.image,
};
setCookie(TOKEN_KEY, payload.accessToken, 7);
localStorage.setItem(USER_KEY, JSON.stringify(state.user));
},
logout(state) {
state.user = null;
state.token = null;
deleteCookie(TOKEN_KEY);
localStorage.removeItem(USER_KEY);
},
},
});
export const { setCredentials, logout } = authSlice.actions;
export default authSlice.reducer;
Chunks explained
| Chunk | What it does |
|---|---|
token from cookie |
Restored with getCookie on load |
user from localStorage |
Restored with JSON.parse on load |
setCredentials |
Writes both stores after a successful login |
logout |
Clears Redux + cookie + localStorage |
| Selectors | Reusable auth checks for routes later |
Storage map
| Field | Storage | Key |
|---|---|---|
| Access token | Cookie | hireboard-token |
| User object | localStorage |
hireboard-user |
3. Register the slice in src/store/index.js
import { configureStore } from "@reduxjs/toolkit";
import uiReducer from "./uiSlice";
import authReducer from "./authSlice";
export const store = configureStore({
reducer: {
ui: uiReducer,
auth: authReducer,
},
});
State shape:
{
ui: { theme },
auth: { user, token },
}
4. LoginForm.jsx
Create src/components/LoginForm.jsx:
import { useState } from "react";
function LoginForm() {
const [username, setUsername] = useState("emilys");
const [password, setPassword] = useState("emilyspass");
function handleSubmit(event) {
event.preventDefault();
// RTK Query login mutation is added in the next lesson
console.log("Login will call API with:", { username, password });
}
return (
<div className="mx-auto max-w-md rounded-xl border border-slate-200 bg-card p-8 shadow-sm dark:border-slate-700 dark:bg-slate-900">
<h2 className="text-2xl font-semibold text-ink dark:text-white">
Sign in to HireBoard
</h2>
<p className="mt-2 text-sm text-muted dark:text-slate-400">
Use the DummyJSON demo account pre-filled below.
</p>
<form onSubmit={handleSubmit} className="mt-6 space-y-4">
<div>
<label
htmlFor="username"
className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300"
>
Username
</label>
<input
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm outline-none ring-brand-500 focus:ring-2 dark:border-slate-600 dark:bg-slate-950"
autoComplete="username"
/>
</div>
<div>
<label
htmlFor="password"
className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300"
>
Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm outline-none ring-brand-500 focus:ring-2 dark:border-slate-600 dark:bg-slate-950"
autoComplete="current-password"
/>
</div>
<button
type="submit"
className="w-full rounded-md bg-brand-600 px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-brand-700"
>
Sign in
</button>
</form>
</div>
);
}
export default LoginForm;
5. Show the form in App.jsx for now
import { useEffect } from "react";
import { useSelector } from "react-redux";
import Navbar from "./components/Navbar";
import LoginForm from "./components/LoginForm";
function App() {
const theme = useSelector((state) => state.ui.theme);
const token = useSelector((state) => state.auth.token);
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">
{token ? (
<p className="text-muted dark:text-slate-400">
Signed in. Real routes arrive in the TanStack Router lesson.
</p>
) : (
<LoginForm />
)}
</main>
</div>
);
}
export default App;
Temporary gating only , TanStack Router will own this properly soon.
Check
- DevTools → Application: no token cookie yet; after you manually test
setCredentialslater you will see the split. - Login form renders with demo credentials.
- Submit logs credentials to the console.
Assessment: Taskly : Auth slice
Add cookie helpers and an auth slice. Store user in localStorage (taskly-user) and token in a cookie (taskly-token). Build a Tailwind LoginForm shell (API call comes next).
Done when: login form renders; slice has user, token, setCredentials, logout.
Next: real DummyJSON login via RTK Query mutation.