When fetching data from an API inside a React component, we typically have to manage three main things in state:
Loading → Are we still waiting for data?
Data → What we got from the API.
Error → Any issue that happened during the fetch.
Step-by-Step Approach
1. Set Up State Variables
We’ll use three useState hooks:
const [loading, setLoading] = useState(true);
const [data, setData] = useState(null);
const [error, setError] = useState('');loading: starts astrueuntil the API responds.data: stores the fetched result; starts asnull(or an empty object).error: stores any error message; starts empty.
2. Fetch Data on Component Mount
Use useEffect with an empty dependency array ([]) so the fetch runs only once after the component renders.
useEffect(() => {
fetch('<https://jsonplaceholder.typicode.com/posts/1>')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not okay');
}
return response.json();
})
.then(result => {
setLoading(false);
setData(result);
setError('');
})
.catch(err => {
setLoading(false);
setData(null);
setError('Something went wrong: ' + err.message);
});
}, []);On success:
loadingbecomesfalsedatais updated with API resulterroris cleared
On failure:
loadingbecomesfalsedatais set tonullerroris updated with a message
3. Rendering Based on State
We show different UI elements depending on our state’s values:
if (loading) return <p>Loading...</p>;
if (error) return <p>{error}</p>;
return <h1>{data.title}</h1>;Why This Works Well
Clear separation of concerns: Each piece of state controls one aspect of the UI (loading spinner, data, error).
Predictable data flow: The UI always reflects the exact fetch state.
Graceful error handling: If something fails, the user gets immediate feedback.
One-time fetch: The empty dependency array in
useEffectensures the API is called only once.
✅ When to Consider useReducer Instead
If you have more complex fetch logic, or multiple related states (loading/data/error) that you want to manage as a single object, useReducer can simplify your state updates and make them more predictable, especially for larger applications.
Example: Data Fetching with useState (Loading, Data, Error States)
<div class="infographic-postfetch" id="pf">
<style>
.infographic-postfetch {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--line: #ddd6c9;
--accent: #2563eb;
--radius: 8px;
--mono-font: 'IBM Plex Mono', monospace;
--body-font: 'Inter', system-ui, sans-serif;
max-width: 720px;
margin: 0 auto;
box-sizing: border-box;
background: transparent;
color: var(--ink);
font-family: var(--body-font);
}
.infographic-postfetch[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--line: #2c313d;
--accent: #5b8def;
}
.infographic-postfetch * { box-sizing: border-box; min-width: 0; }
.pf-grid { display: flex; flex-direction: column; gap: 1.1rem; align-items: stretch; }
.pf-code-block pre {
margin: 0; background: #0d0d10; border: 1px solid #24252d; border-radius: var(--radius);
padding: 0.9rem; overflow: auto; max-height: 420px;
}
.pf-code-block code { font-family: var(--mono-font); font-size: 0.72rem; line-height: 1.7; color: #d4d4d8; white-space: pre; }
.tok-kw { color: #c678dd; }
.tok-tag { color: #e06c75; }
.tok-type { color: #61afef; }
.tok-attr { color: #d19a66; }
.tok-str { color: #98c379; }
.tok-punc { color: #6b7280; }
.tok-comment { color: #7f848e; font-style: italic; }
.pf-output-label {
font-family: var(--mono-font); font-size: 0.68rem; letter-spacing: 0.1em; text-transform: uppercase;
color: var(--ink-soft); text-align: center;
}
.pf-preview { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; box-shadow: 0 10px 24px -12px rgba(26,26,26,0.18); }
.pf-preview-bar { background: #e9e9e9; padding: 0.45rem 0.6rem; display: flex; gap: 0.3rem; }
.pf-preview-bar span { width: 8px; height: 8px; border-radius: 50%; display: block; }
.pf-preview-bar span:nth-child(1) { background: #ff5f56; }
.pf-preview-bar span:nth-child(2) { background: #ffbd2e; }
.pf-preview-bar span:nth-child(3) { background: #27c93f; }
.pf-preview-page { background: #fff; padding: 1.6rem; color: #000; }
.pf-preview-page h1 { font-size: 1.15rem; margin: 0 0 0.6rem; }
.pf-preview-page p { margin: 0; font-size: 0.9rem; line-height: 1.5; color: #333; }
.pf-loading { color: #666; font-size: 0.9rem; margin: 0; }
.pf-error { color: #dc2626; font-size: 0.9rem; margin: 0; }
.pf-controls { display: flex; justify-content: center; margin-top: 1rem; }
.pf-btn {
font-family: var(--body-font); font-size: 0.8rem; font-weight: 600; color: #fff; background: var(--accent);
border: none; border-radius: 100px; padding: 0.5rem 1rem; cursor: pointer; transition: transform 0.15s;
}
.pf-btn:hover { transform: translateY(-1px); }
@media (prefers-reduced-motion: reduce) {
.infographic-postfetch *, .infographic-postfetch *::before, .infographic-postfetch *::after {
transition-duration: 0.001ms !important; animation-duration: 0.001ms !important;
}
}
</style>
<div class="pf-grid">
<div class="pf-code-block">
<pre><code id="pfCode"></code></pre>
</div>
<p class="pf-output-label">OUTPUT</p>
<div class="pf-preview">
<div class="pf-preview-bar"><span></span><span></span><span></span></div>
<div class="pf-preview-page" id="pfBody">
<p class="pf-loading">Loading...</p>
</div>
</div>
<div class="pf-controls">
<button class="pf-btn" id="pfErrorBtn" type="button">Simulate a failed fetch</button>
</div>
</div>
</div>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<script>
(function () {
var root = document.getElementById('pf');
if (!root) return;
var bodyAttr = document.body.getAttribute('data-darkmode');
var dark;
if (bodyAttr === 'true') dark = true;
else if (bodyAttr === 'false') dark = false;
else {
var match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) root.setAttribute('data-theme', 'dark');
window.addEventListener('message', function (e) {
if (e.data && e.data.type === 'darkMode') {
if (e.data.value) root.setAttribute('data-theme', 'dark');
else root.removeAttribute('data-theme');
}
});
var code =
'<span class="tok-kw">import</span> <span class="tok-type">React</span><span class="tok-punc">,</span> <span class="tok-punc">{</span> useState<span class="tok-punc">,</span> useEffect <span class="tok-punc">}</span> <span class="tok-kw">from</span> <span class="tok-str">\'react\'</span><span class="tok-punc">;</span>\n\n' +
'<span class="tok-kw">function</span> <span class="tok-type">PostFetcher</span><span class="tok-punc">() {</span>\n' +
' <span class="tok-kw">const</span> <span class="tok-punc">[</span>loading<span class="tok-punc">,</span> setLoading<span class="tok-punc">]</span> <span class="tok-punc">=</span> useState<span class="tok-punc">(</span><span class="tok-kw">true</span><span class="tok-punc">);</span>\n' +
' <span class="tok-kw">const</span> <span class="tok-punc">[</span>data<span class="tok-punc">,</span> setData<span class="tok-punc">]</span> <span class="tok-punc">=</span> useState<span class="tok-punc">(</span><span class="tok-kw">null</span><span class="tok-punc">);</span>\n' +
' <span class="tok-kw">const</span> <span class="tok-punc">[</span>error<span class="tok-punc">,</span> setError<span class="tok-punc">]</span> <span class="tok-punc">=</span> useState<span class="tok-punc">(</span><span class="tok-str">\'\'</span><span class="tok-punc">);</span>\n\n' +
' useEffect<span class="tok-punc">(() => {</span>\n' +
' fetch<span class="tok-punc">(</span><span class="tok-str">\'/api/posts/1\'</span><span class="tok-punc">)</span>\n' +
' .then<span class="tok-punc">(</span>response <span class="tok-punc">=> {</span>\n' +
' <span class="tok-kw">if</span> <span class="tok-punc">(!</span>response.ok<span class="tok-punc">) {</span>\n' +
' <span class="tok-kw">throw</span> <span class="tok-kw">new</span> Error<span class="tok-punc">(</span><span class="tok-str">\'Network response was not OK\'</span><span class="tok-punc">);</span>\n' +
' <span class="tok-punc">}</span>\n' +
' <span class="tok-kw">return</span> response.json<span class="tok-punc">();</span>\n' +
' <span class="tok-punc">})</span>\n' +
' .then<span class="tok-punc">(</span>result <span class="tok-punc">=> {</span>\n' +
' setLoading<span class="tok-punc">(</span><span class="tok-kw">false</span><span class="tok-punc">);</span>\n' +
' setData<span class="tok-punc">(</span>result<span class="tok-punc">);</span>\n' +
' setError<span class="tok-punc">(</span><span class="tok-str">\'\'</span><span class="tok-punc">);</span>\n' +
' <span class="tok-punc">})</span>\n' +
' .catch<span class="tok-punc">(</span>err <span class="tok-punc">=> {</span>\n' +
' setLoading<span class="tok-punc">(</span><span class="tok-kw">false</span><span class="tok-punc">);</span>\n' +
' setData<span class="tok-punc">(</span><span class="tok-kw">null</span><span class="tok-punc">);</span>\n' +
' setError<span class="tok-punc">(</span><span class="tok-str">\'Something went wrong: \'</span> <span class="tok-punc">+</span> err.message<span class="tok-punc">);</span>\n' +
' <span class="tok-punc">});</span>\n' +
' <span class="tok-punc">},</span> <span class="tok-punc">[]);</span>\n\n' +
' <span class="tok-kw">if</span> <span class="tok-punc">(</span>loading<span class="tok-punc">) {</span>\n' +
' <span class="tok-kw">return</span> <span class="tok-punc"><</span><span class="tok-tag">p</span><span class="tok-punc">></span>Loading...<span class="tok-punc"></</span><span class="tok-tag">p</span><span class="tok-punc">>;</span>\n' +
' <span class="tok-punc">}</span>\n\n' +
' <span class="tok-kw">if</span> <span class="tok-punc">(</span>error<span class="tok-punc">) {</span>\n' +
' <span class="tok-kw">return</span> <span class="tok-punc"><</span><span class="tok-tag">p</span> <span class="tok-attr">style</span><span class="tok-punc">={{</span> <span class="tok-attr">color</span><span class="tok-punc">:</span> <span class="tok-str">\'red\'</span> <span class="tok-punc">}}>{</span>error<span class="tok-punc">}</</span><span class="tok-tag">p</span><span class="tok-punc">>;</span>\n' +
' <span class="tok-punc">}</span>\n\n' +
' <span class="tok-kw">return</span> <span class="tok-punc">(</span>\n' +
' <span class="tok-punc"><</span><span class="tok-tag">div</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc"><</span><span class="tok-tag">h1</span><span class="tok-punc">>{</span>data.title<span class="tok-punc">}</</span><span class="tok-tag">h1</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc"><</span><span class="tok-tag">p</span><span class="tok-punc">>{</span>data.body<span class="tok-punc">}</</span><span class="tok-tag">p</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc"></</span><span class="tok-tag">div</span><span class="tok-punc">></span>\n' +
' <span class="tok-punc">);</span>\n' +
'<span class="tok-punc">}</span>\n\n' +
'<span class="tok-kw">export</span> <span class="tok-kw">default</span> <span class="tok-type">PostFetcher</span><span class="tok-punc">;</span>';
root.querySelector('#pfCode').innerHTML = code;
var out = root.querySelector('#pfBody');
var errorBtn = root.querySelector('#pfErrorBtn');
var samplePost = {
title: 'sunt aut facere repellat provident occaecati excepturi optio',
body: 'quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto'
};
function showLoading() {
out.innerHTML = '<p class="pf-loading">Loading...</p>';
}
function showSuccess() {
out.innerHTML =
'<h1>' + samplePost.title + '</h1>' +
'<p>' + samplePost.body + '</p>';
}
function showError() {
out.innerHTML = '<p class="pf-error">Something went wrong: Network response was not OK</p>';
}
// initial "fetch" — sample data standing in for the real request
showLoading();
setTimeout(showSuccess, 700);
// lets you see the other branch of the if/else without touching the network
errorBtn.addEventListener('click', function () {
showLoading();
setTimeout(showError, 500);
});
})();
</script>How This Works
Initial State
loadingstarts astruedataisnullinitiallyerroris an empty string
Fetching Data
useEffecttriggers the fetch once when the component mounts.On success, we stop loading and set
data.On failure, we stop loading and set an error message.
UI Feedback
If still loading → shows
"Loading...".If error → displays error in red.
If success → renders the requested post (title + body).