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:

  1. Application → Cookieshireboard-token exists.
  2. Application → Local Storagehireboard-user is a JSON object with firstName, etc.
  3. Redux state: auth.token and auth.user match.

Check

  • Network tab shows POST .../auth/login with 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.