Goal
Log in with a real DummyJSON API call using an RTK Query mutation. On success, store the user in localStorage and the token in a cookie via setCredentials.
1. Query vs mutation
| Tool | Best for |
|---|---|
| RTK Query query | GET-style reads you want cached (jobs) |
| RTK Query mutation | Writes / one-shot POSTs (login) |
2. Create src/apis/loginApi.js
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import { getCookie } from "../utils/cookies";
export const loginApi = createApi({
reducerPath: "loginApi",
baseQuery: fetchBaseQuery({
baseUrl: "https://dummyjson.com",
}),
endpoints: (builder) => ({
login: builder.mutation({
query: ({ username, password }) => ({
url: "/auth/login",
method: "POST",
body: { username, password, expiresInMins: 60 },
}),
}),
}),
});
export const { useLoginMutation } = loginApi;
Chunks explained
| Chunk | What it does |
|---|---|
baseUrl: "https://dummyjson.com" |
Real public API host |
prepareHeaders |
Attaches Bearer token on later authenticated requests |
getState().auth.token || getCookie(...) |
Works even before React rehydrates UI |
POST /auth/login |
Real network login |
useLoginMutation |
Hook: [trigger, result] |
3. Register the API in src/store/index.js
import { configureStore } from "@reduxjs/toolkit";
import uiReducer from "./uiSlice";
import authReducer from "./authSlice";
import { loginApi } from "../apis/loginApi";
import { jobsApi } from "../apis/jobsApi";
export const store = configureStore({
reducer: {
ui: uiReducer,
auth: authReducer,
[loginApi.reducerPath]: loginApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(loginApi.middleware),
});
4. Wire LoginForm.jsx
import { useState } from "react";
import { useDispatch } from "react-redux";
import { setCredentials } from "../store/authSlice";
import { useLoginMutation } from "../apis/loginApi";
function LoginForm() {
const dispatch = useDispatch();
const [login, { isLoading, error, reset }] = useLoginMutation();
const [username, setUsername] = useState("emilys");
const [password, setPassword] = useState("emilyspass");
async function handleSubmit(event) {
event.preventDefault();
reset();
try {
const data = await login({ username, password }).unwrap();
dispatch(setCredentials(data));
} catch {
// `error` from the hook updates automatically
}
}
const errorMessage =
typeof error?.data === "string"
? error.data
: error?.data?.message || (error ? "Login failed" : null);
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">
Real API: POST https://dummyjson.com/auth/login
</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>
{errorMessage && (
<p className="rounded-md bg-red-50 px-3 py-2 text-sm text-red-700 dark:bg-red-950 dark:text-red-300">
{errorMessage}
</p>
)}
<button
type="submit"
disabled={isLoading}
className="w-full rounded-md bg-brand-600 px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-brand-700 disabled:cursor-not-allowed disabled:opacity-60"
>
{isLoading ? "Signing in…" : "Sign in"}
</button>
</form>
</div>
);
}
export default LoginForm;
Chunks explained
| Chunk | What it does |
|---|---|
login(...).unwrap() |
Real HTTP; throws if the API returns an error |
dispatch(setCredentials(data)) |
Saves user → localStorage, token → cookie |
isLoading |
Built-in pending flag from the mutation |
5. Verify storage after login
In browser DevTools:
- Application → Cookies →
hireboard-tokenexists. - Application → Local Storage →
hireboard-useris a JSON object withfirstName, etc. - Redux state:
auth.tokenandauth.usermatch.
Check
- Network tab shows
POST .../auth/loginwith status 200. - Wrong password shows an error under the form.
- Refresh keeps you signed in (cookie + localStorage hydrate the slice).
Assessment: Taskly : Login mutation
Wire DummyJSON POST /auth/login with an RTK Query mutation. On success call setCredentials. Use emilys / emilyspass. Attach Bearer token in prepareHeaders when present.
Done when: real login works; taskly-token cookie and taskly-user exist after success.
Next: TanStack Router with protected routes.