A fetch is not instant. If you only have const [data, setData] = useState(null) and render data.title, the first paint crashes or shows nothing useful.
function Todo() {
const [status, setStatus] = useState('loading'); // loading | error | ok
const [todo, setTodo] = useState(null);
const [message, setMessage] = useState('');
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((r) => {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then((json) => { setTodo(json); setStatus('ok'); })
.catch((e) => { setMessage(e.message); setStatus('error'); });
}, []);
if (status === 'loading') return <p>Loading…</p>;
if (status === 'error') return <p>Could not load: {message}</p>;
return <p>{todo.title}</p>;
}jsonplaceholder.typicode.com is a public demo API. Real apps need your own backend (see the Node course) and CORS configured on that server — the browser will block random APIs that do not send Access-Control-Allow-Origin.