Goal
Fetch real job listings from JSONFakery (GET /jobs), normalize them into HireBoard job cards with transformResponse, and render them on the protected /jobs route.
1. Create src/apis/jobsApi.js
Add getJobs against the live JSONFakery API:
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import { getCookie } from "../utils/cookies";
function mapJsonFakeryJob(job) {
return {
id: job.id,
title: job.title,
company: job.company,
location: job.location,
type: job.employment_type || "Full-time",
salary:
job.salary_from && job.salary_to
? `$${job.salary_from.toLocaleString()} - $${job.salary_to.toLocaleString()}`
: "$--",
tags: [job.job_category, job.is_remote_work ? "Remote" : "Onsite"].filter(
Boolean,
),
description: job.description,
};
}
export const jobsApi = createApi({
reducerPath: "jobsApi",
baseQuery: fetchBaseQuery({
baseUrl: "https://jsonfakery.com",
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token || getCookie("hireboard-token");
if (token) {
headers.set("Authorization", `Bearer ${token}`);
}
return headers;
},
}),
tagTypes: ["Jobs"],
endpoints: (builder) => ({
login: builder.mutation({
query: ({ username, password }) => ({
url: "/auth/login",
method: "POST",
body: { username, password, expiresInMins: 60 },
}),
}),
getJobs: builder.query({
query: () => "/jobs",
transformResponse: (response) => (response || []).map(mapJsonFakeryJob),
providesTags: ["Jobs"],
}),
}),
});
export const { useGetJobsQuery } = jobsApi;
Chunks explained
| Chunk | What it does |
|---|---|
GET /jobs |
Reads live job data from JSONFakery |
transformResponse |
Normalizes API job fields for the UI |
mapJsonFakeryJob |
Converts salary_from, salary_to, job_category, and more |
providesTags: ["Jobs"] |
Cache label for later invalidation |
| Bearer header | Reuses the cookie/token from login |
JSONFakery returns a real jobs array, so we no longer need dummy product mapping. The important part is still a live network request + RTK Query caching.
2. JobCard.jsx
function JobCard({ job }) {
return (
<article className="flex flex-col rounded-xl border border-slate-200 bg-card p-5 shadow-sm transition hover:-translate-y-0.5 hover:shadow-md dark:border-slate-700 dark:bg-slate-900">
<div className="mb-3 flex items-start justify-between gap-3">
<div>
<h3 className="text-lg font-semibold text-ink dark:text-white">
{job.title}
</h3>
<p className="text-sm text-brand-600">{job.company}</p>
</div>
<span className="rounded-full bg-brand-50 px-2.5 py-1 text-xs font-medium capitalize text-brand-700 dark:bg-brand-900/40 dark:text-brand-100">
{job.type}
</span>
</div>
<p className="mb-4 flex-1 text-sm text-muted dark:text-slate-400">
{job.description}
</p>
<div className="mb-4 flex flex-wrap gap-2">
{job.tags.map((tag) => (
<span
key={tag}
className="rounded-md bg-slate-100 px-2 py-1 text-xs capitalize text-slate-600 dark:bg-slate-800 dark:text-slate-300"
>
{tag}
</span>
))}
</div>
<div className="flex items-center justify-between border-t border-slate-100 pt-3 text-sm dark:border-slate-800">
<span className="text-muted dark:text-slate-400">{job.location}</span>
<span className="font-medium text-ink dark:text-white">
{job.salary}
</span>
</div>
</article>
);
}
export default JobCard;
3. JobsList.jsx
import { useGetJobsQuery } from "../apis/jobsApi";
import JobCard from "./JobCard";
function JobsList() {
const { data: jobs, isLoading, isError, error, refetch } = useGetJobsQuery();
if (isLoading) {
return (
<p className="text-muted dark:text-slate-400" role="status">
Loading open roles from JSONFakery…
</p>
);
}
if (isError) {
return (
<div className="rounded-md bg-red-50 p-4 text-sm text-red-700 dark:bg-red-950 dark:text-red-300">
<p>
Could not load jobs: {error?.error || error?.data || "Unknown error"}
</p>
<button
type="button"
onClick={refetch}
className="mt-2 font-medium underline"
>
Try again
</button>
</div>
);
}
return (
<section>
<div className="mb-6 flex items-end justify-between gap-4">
<div>
<h2 className="text-2xl font-semibold">Open roles</h2>
<p className="text-sm text-muted dark:text-slate-400">
{jobs.length} positions · GET /jobs (live API)
</p>
</div>
<button
type="button"
onClick={refetch}
className="rounded-md border border-slate-300 px-3 py-2 text-sm font-medium hover:bg-slate-100 dark:border-slate-600 dark:hover:bg-slate-800"
>
Refresh
</button>
</div>
<div className="grid gap-4 sm:grid-cols-2">
{jobs.map((job) => (
<JobCard key={job.id} job={job} />
))}
</div>
</section>
);
}
export default JobsList;
Hook chunks explained
| Chunk | What it does |
|---|---|
useGetJobsQuery() |
Fires (or reads cache for) the live request |
isLoading / isError |
Request lifecycle UI |
refetch |
Hit the network again |
4. Use it on the jobs route
Replace the placeholder in src/routes/_authenticated/jobs.jsx:
import { createFileRoute } from "@tanstack/react-router";
import JobsList from "../../components/JobsList";
export const Route = createFileRoute("/_authenticated/jobs")({
component: JobsPage,
});
function JobsPage() {
return <JobsList />;
}
5. Confirm in DevTools
- Network →
jobs→ status 200. - Redux →
jobsApi.queriesshows cached data. - Visiting
/jobswhile logged out still redirects to/login.
Check
- Signed-in
/jobsshows a grid from the live API. - Refresh button triggers another network call (or serves cache, then refetch).
Assessment: Taskly : RTK Query todos
Fetch real todos with an RTK Query query:
GET https://dummyjson.com/todos?limit=20
Map each item into a Taskly card (id, todo text, completed, userId). Show loading and error UI on /tasks. Attach Bearer token via prepareHeaders.
Done when: signed-in /tasks shows a live DummyJSON todo list.
Next: save applications into a client slice and show them on /saved.