The screen so far shows made-up data. Time to connect React to the API you built. Fetching data is a side effect: something that reaches outside the component. React gives you the hook useEffect for that. It runs your code after the component has been drawn, and lets you clean up afterwards.
One place for talking to the API
// One small function that talks to our API, so no component repeats fetch boilerplate.
const TOKEN_KEY = 'jt_token';
export const getToken = () => localStorage.getItem(TOKEN_KEY);
export const setToken = (t) => (t ? localStorage.setItem(TOKEN_KEY, t) : localStorage.removeItem(TOKEN_KEY));
export class ApiError extends Error {
constructor(status, message, details) {
super(message);
this.status = status;
this.details = details; // field errors from the server, e.g. { company: 'company is required' }
}
}
export async function api(path, { method = 'GET', body, signal } = {}) {
const headers = {};
if (body !== undefined) headers['Content-Type'] = 'application/json';
const token = getToken();
if (token) headers.Authorization = `Bearer ${token}`;
const res = await fetch(`/api${path}`, { method, headers, signal, body: body !== undefined ? JSON.stringify(body) : undefined });
if (res.status === 204) return null;
const data = await res.json().catch(() => null);
if (!res.ok) throw new ApiError(res.status, data?.message || `Request failed (${res.status})`, data?.details);
return data;
}
api(path, options)adds the/apiprefix, the JSON header and theAuthorizationtoken, so no component repeats that.- It throws an
ApiErrorwith the status, the message and the field details from the server whenever the answer is not OK. Remember thatfetchitself does not fail on 400 or 500 (lesson 4). 204 No Contentreturnsnull, because there is no body to parse.- The token is read from
localStorage. Lesson 32 sets it when the person signs in.
useEffect
import { useEffect, useState } from 'react';
import Header from './components/Header.jsx';
import JobCard from './components/JobCard.jsx';
import JobForm from './components/JobForm.jsx';
import { api, setToken } from './api.js';
export default function App() {
const [jobs, setJobs] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
// useEffect runs AFTER the screen is drawn. The [] means "only once, when the component first appears".
useEffect(() => {
const controller = new AbortController(); // lets us cancel the request if the component goes away
async function load() {
try {
// TEMPORARY: sign in as the demo user from `npm run seed` so we have a token.
// Lesson 32 replaces this with a real sign-in page.
const login = await api('/auth/login', { method: 'POST', body: { email: 'demo@example.com', password: 'Demo12345' }, signal: controller.signal });
setToken(login.token);
const page = await api('/jobs?limit=50', { signal: controller.signal });
setJobs(page.items);
} catch (err) {
if (err.name !== 'AbortError') setError(err.message);
} finally {
setLoading(false);
}
}
load();
return () => controller.abort(); // cleanup function: runs when the component is removed
}, []);
const changeStatus = (id, status) => setJobs((cur) => cur.map((j) => (j.id === id ? { ...j, status } : j)));
// The form still only changes the screen here. Lesson 31 saves jobs on the server.
const addJob = (values) => setJobs((cur) => [{ ...values, id: `local-${Date.now()}` }, ...cur]);
return (
<>
<Header />
<main className="container">
<h1>My jobs ({jobs.length})</h1>
<JobForm onSubmit={addJob} />
{loading && <p className="muted">Loading your jobs…</p>}
{error && <p className="error" role="alert">{error} (is the server running, and did you run npm run seed?)</p>}
{!loading && !error && jobs.length === 0 && <p className="muted">No jobs yet. Add your first one above.</p>}
{jobs.map((job) => <JobCard key={job.id} job={job} onStatusChange={changeStatus} />)}
</main>
</>
);
}
useEffect(fn, [dependencies]) runs fn after drawing. The dependency array controls when it runs again. With [] it runs once, when the component first appears. With [id] it runs again whenever id changes. With no array it runs after every draw, which is rarely what you want. The rule that avoids most bugs: list every value from the component that the effect uses.
The function you return from the effect is its cleanup. It runs before the next run and when the component disappears. Here we use it to call controller.abort(). An AbortController cancels a request that is no longer needed, for example when the person leaves the page before the answer arrives. Without it, the late answer would try to update a component that no longer exists, or, worse, an older request could finish after a newer one and show stale data (a *race condition*).
Three states: loading, error, data
Network calls take time and can fail. The component keeps three pieces of state, jobs, loading and error, and draws something sensible for each. A blank screen is not an acceptable loading state, and a silent failure is not an acceptable error state. The error message ends with a hint about the likely cause, which saves a beginner a lot of time.
The effect cannot be an async function directly, because it must return either nothing or a cleanup function, and an async function returns a promise. So we define an async function load() inside and call it, as the file shows.
What about the form?
It still only changes the screen, and is marked with a comment. Saving through the API comes in lesson 31, after you have met routing and shared state. Reload the page and the form's jobs disappear, while the server's jobs return. That difference is worth seeing with your own eyes.
Your task
- Start the server, run
npm run seed, then start the client. You should see 30 jobs from your database. - Stop the server and reload. Read the error message. Start it again.
- Slow the network in the browser's Network tab (choose “Slow 3G”) and watch the loading message.
- Add
console.log('effect ran')inside the effect. Why does it print twice in development? (Hint: lesson 25.)
How to check it worked
- The list shows the jobs from MongoDB.
- With the server stopped, a clear error replaces the list.
- Reloading brings back the saved jobs, not the ones you added on screen.
Summary
- Wrap
fetchin oneapi()helper that sets headers and throwsApiError. useEffectruns after drawing; the dependency array says when to run again; the returned function cleans up.- Always draw loading, error and empty states.