useEffect - Side Effects, Data Fetching & Async State
Browser DevTools - The Network Tab
You've already used the Elements, Console, and Accessibility panels in Week 1. This chapter goes deeper on one panel in particular: the Network tab. Now that you're fetching data from APIs inside useEffect, the Network tab becomes one of your most important debugging tools: it shows you exactly what your code is doing when it talks to a server.
When your component fetches data and something goes wrong, there are two possible culprits: your React code, or the API request itself. The Network tab tells you which. If the request never goes out, or comes back with an error status, or returns unexpected data you'll see it here before you even look at your component code.
Professionals keep the Network tab open by default when building anything that touches an API. It becomes second nature.
Open DevTools (F12 or Cmd + Option + I on Mac), then click the Network tab. If it looks empty, refresh the page: the Network tab only captures requests made while it's open.
You'll see a list of every resource the browser requested: HTML, CSS, JavaScript files, images, and — most relevant to you now — API calls.
Each row in the list shows:
fetch, document, script, img, etc.)For API calls from useEffect, the type will show as fetch or xhr.
<aside> 💡
Info
The filter bar at the top lets you narrow down what you see. When you're debugging API calls, click Fetch/XHR — this hides all the HTML, CSS, and JS files and shows only requests made by JavaScript code. Much cleaner.
</aside>
When your useEffect runs a fetch, it shows up in the Network tab almost immediately. Here's how to find it:
useEffect to run)Click the request row to open a detail panel on the right. This is where most of the useful information lives.
The Headers tab shows the full picture of the request and response:
GET, POST, etc.200 OK, 404 Not Found)When you're debugging a failed fetch, start here. Is the URL correct? Did you send the right headers? What status code came back?
This shows the raw data the server sent back. If your API is supposed to return JSON and you're seeing an error message instead, or an HTML page, this tab will show you that immediately.
For a successful JSON response, you'll see the raw JSON string here. Click Preview for a formatted, collapsible view that's much easier to read — this is especially useful when the response is a large nested object.
<aside> ⌨️
Hands On
Build a simple component that fetches from a public API — for example https://api.github.com/users/github (replace github with any username). Open the Network tab filtered to Fetch/XHR, load your component, and click the request row. Explore the Headers and Preview tabs. Can you find the user's name, location, and number of public repos in the response?
</aside>
The status code is the first thing to check when a fetch doesn't behave as expected. You've likely seen 404 before, but there's a whole language of status codes. Here are the ones you'll encounter regularly:
| Code | Meaning | What it usually means in practice |
|---|---|---|
200 |
OK | Request succeeded, data is in the response |
201 |
Created | A new resource was created (common after POST) |
204 |
No Content | Succeeded but no data returned |
400 |
Bad Request | You sent something the server didn't expect |
401 |
Unauthorized | Missing or invalid authentication |
403 |
Forbidden | Authenticated but not allowed to access this |
404 |
Not Found | The resource doesn't exist at that URL |
429 |
Too Many Requests | You've hit a rate limit |
500 |
Internal Server Error | Something went wrong on the server |
The codes fall into ranges: 2xx is success, 3xx is redirect, 4xx is a client error (your request had a problem), 5xx is a server error (their side broke).
This is why the fetch pattern from the useEffect lesson checks response.ok before parsing the JSON:
const response = await fetch('<https://api.example.com/data>');
if (!response.ok) {
// response.ok is false for anything outside 200-299
throw new Error(`Request failed with status ${response.status}`);
}
const data = await response.json();
fetch does not throw on non-2xx status codes — a 404 or 500 response resolves the Promise normally. Without this check, your code would try to parse the error page as JSON and fail with a confusing error message. The Network tab makes this immediately visible: you can see the 404 in the status column before you even read your code.
The Network tab is the best way to verify that your useEffect and its dependency array are working correctly. Here are some specific things to watch for.
After you load a component that fetches on mount (with [] as the dependency array), you should see exactly one request. If you see two, check whether you're in React's Strict Mode because in development, Strict Mode intentionally mounts components twice to help catch bugs, which means effects also run twice. This is expected and only happens in development.
When your useEffect depends on a prop or state variable, it should re-fetch when that value changes. The Network tab is the proof: change the value (e.g. click a button that changes a userId), and you should see a new request appear with the updated URL.
// This effect re-fetches whenever userId changes
useEffect(() => {
fetch(`https://api.example.com/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
Open the Network tab, filter to Fetch/XHR, and click through different user IDs. Each click should produce a new row.
If you implemented AbortController cleanup in your useEffect (as covered in the useEffect lesson), you can see aborted requests in the Network tab too. They show up as cancelled in the Status column — a red (cancelled) label.
This is useful for confirming your cleanup is working. Quickly change the userId a few times and check: the previous requests should be marked cancelled, and only the most recent one should complete with a 200.
<aside> ⌨️
Hands On
Build a component with a dropdown or set of buttons that changes a userId prop. Connect it to a useEffect that fetches from an API based on that ID, and add AbortController cleanup. Open the Network tab filtered to Fetch/XHR and click through the options quickly. You should see cancelled requests appear for the ones that were aborted before they completed. If you don't see cancellations, check that your cleanup function is calling controller.abort().
</aside>
Click the Timing tab in a request's detail panel. You'll see a breakdown of where the time went:
For most API calls, TTFB is what matters. A high TTFB means the server is slow — your network code is fine, but the API is taking a while to respond. This is useful to know when debugging performance issues, because it tells you whether the bottleneck is on your side or theirs.
The throttling dropdown (in the Network tab toolbar) lets you simulate slower network conditions. This is important because your laptop on a fast connection is not how everyone uses your app.
Set it to Slow 3G and reload your component. Watch how your loading state looks and how long it takes before data appears. This is exactly why the loading state you built in the useEffect lesson matters — without it, users on slow connections just see a blank or broken-looking page.
<aside> ⌨️
</aside>
The Network tab has a waterfall column on the far right showing horizontal bars for each request. The position shows when the request started, and the length shows how long it took.
When multiple requests run in parallel, their bars overlap horizontally. When one request depends on another (like fetching a user and then fetching their posts), you'll see the bars in sequence — the second one starts only after the first finishes.
This visualisation is useful for spotting unnecessary sequential fetches. If two requests could run in parallel but are running in sequence, that's avoidable loading time.
If you trigger a component and no request appears in the Network tab: