3.1: Fetch and useEffect
Learning Objectives
- Explain why data fetching belongs in an Effect instead of directly inside render
- Fetch remote data with
async/awaitusing a safeuseEffectpattern - Choose dependency arrays that match the values an Effect actually uses
- Diagnose common
useEffectmistakes such as infinite loops, stale values, and missing cleanup
Why useEffect Exists
Section titled “Why useEffect Exists”React components are supposed to be pure: given the same props and state, they should return the same UI. A network request is not pure. It reaches outside your component and asks another system for data, so it belongs in an Effect.1
That makes useEffect a good fit for work like:
- fetching API data
- subscribing to native events
- starting timers
- cleaning up anything that should stop when the screen changes
It is not the right tool for everything. If you are only calculating a value from existing state, you usually do not need an Effect.2
The Basic Fetch Pattern
Section titled “The Basic Fetch Pattern”The most common beginner pattern is: render once, fetch after render, then update state with the response.3
const [movies, setMovies] = useState([]);const [isLoading, setIsLoading] = useState(true);const [error, setError] = useState<string | null>(null);
useEffect(() => { let ignore = false;
async function loadMovies() { try { setIsLoading(true); setError(null); const response = await fetch('https://reactnative.dev/movies.json'); const json = await response.json();
if (!ignore) { setMovies(json.movies); } } catch (caughtError) { if (!ignore) { setError('Could not load movies.'); } } finally { if (!ignore) { setIsLoading(false); } } }
void loadMovies();
return () => { ignore = true; };}, []);Deeper Dive: Why is there `void` before `loadMovies()`?
loadMovies() is an async function, which means calling it returns a Promise.
loadMovies();This will still run, but TypeScript or ESLint may warn that you created a Promise and then ignored it. That is often called a floating promise.
void loadMovies();The void operator4 means: run this expression, but intentionally throw away its return value. In this case, it tells the reader and the linter, “Yes, I know this returns a Promise, and I am intentionally not awaiting it here.”
Why not just use await loadMovies()? Because the function you pass to useEffect should not be async. React expects an Effect to return either nothing or a cleanup function, not a Promise.
So this pattern:
- starts the async work
- avoids a floating-promise warning
- keeps the Effect callback in the correct shape for React
Two details matter here:
- The Effect callback itself is not marked
async. Instead, create an async function inside the Effect and call it. - The cleanup sets
ignore = trueso an older request cannot update state after the component unmounts or the dependencies change.1
Dependency Arrays at a Glance
Section titled “Dependency Arrays at a Glance”| Pattern | When it runs | Common use |
|---|---|---|
useEffect(() => { ... }) | After every render | Rare; easy to misuse |
useEffect(() => { ... }, []) | Once after the first render | Initial fetch on mount |
useEffect(() => { ... }, [query]) | On first render and when query changes | Refetch data for a new search term |
Common useEffect Pitfalls
Section titled “Common useEffect Pitfalls”This is the part students usually struggle with. Most useEffect bugs come from one of the patterns below.
1. Forgetting the Dependency Array
Section titled “1. Forgetting the Dependency Array”If you omit the dependency array, the Effect runs after every render.
useEffect(() => { loadMovies();});That is usually wrong for data fetching. If loadMovies() updates state, the component re-renders, the Effect runs again, and you can create a fetch loop.
2. Depending on the State You Are Setting
Section titled “2. Depending on the State You Are Setting”This is another classic loop:
useEffect(() => { async function loadMovies() { const nextMovies = await getMovies(); setMovies(nextMovies); }
void loadMovies();}, [movies]);The Effect sets movies, which changes movies, which reruns the Effect, which sets movies again. The dependency array should include the values the Effect reads from outside itself, not the state that it only writes.
3. Making the Effect Callback async
Section titled “3. Making the Effect Callback async”This looks tempting, but it is the wrong shape:
useEffect(async () => { const response = await fetch(url); const json = await response.json(); setMovies(json.movies);}, [url]);React expects the Effect callback to return either nothing or a cleanup function. An async function always returns a Promise. The fix is to define an async function inside the Effect and call it.
4. Forgetting Cleanup for Slow Responses
Section titled “4. Forgetting Cleanup for Slow Responses”Imagine a user changes screens before the request finishes. Without cleanup, a stale request may still try to update state.
useEffect(() => { let ignore = false;
async function loadMovies() { const nextMovies = await getMovies(); if (!ignore) { setMovies(nextMovies); } }
void loadMovies();
return () => { ignore = true; };}, []);This small cleanup pattern prevents race-condition bugs where an older response arrives late and overwrites newer state.1
5. Using an Effect for Pure Calculations
Section titled “5. Using an Effect for Pure Calculations”If a value can be calculated during render, do that directly instead of storing it in state with an Effect.
const visibleMovies = movies.filter((movie) => movie.releaseYear === year);You do not need:
const [visibleMovies, setVisibleMovies] = useState<Movie[]>([]);
useEffect(() => { setVisibleMovies(movies.filter((movie) => movie.releaseYear === year));}, [movies, year]);That extra Effect adds more state, more rerenders, and more opportunities for bugs.2
Deeper Dive: Why did my fetch run twice in development?
Some development setups intentionally run Effects more than once to help you notice missing cleanup. If you see two requests in development, do not rush to “silence” the extra run.
Instead, ask:
- Does my Effect clean up correctly?
- Would a stale response cause a bug?
- Is my logic safe if the screen mounts, unmounts, and mounts again?
If your cleanup is correct and your Effect is idempotent, development checks become much less confusing.
Guided Pattern
Section titled “Guided Pattern”When you are writing a fetch Effect from scratch, use this checklist:
- Create state for data, loading, and error.
- Write an async helper inside the Effect instead of marking the Effect callback
async. - Wrap the fetch in
try/catch/finallyso loading and error states always stay consistent. - Return cleanup if the request result should be ignored after unmount or dependency changes.
Exercise
Section titled “Exercise”Practice repairing a full Effect workflow before we move on to JSON parsing and loading indicators in 3.2.
- Click the external link icon (top-left of each Snack) to open in a new tab.
- Click the blue “Save” button to save to your account.
- Copy the Snack URL once completed and submit on Moodle.
The starter app has three issues to fix:
- it fetches only on mount
- loading/error states are incomplete
- fast year switches can show stale results
Hint
Work in this order:
- Make the Effect react to
selectedYear. - Add
isLoadinganderrorhandling around the request (try/catch/finally). - Make an
ignorecleanup flag so a slow older request cannot overwrite the latest selection.
Summary
Section titled “Summary”useEffectis for synchronizing with systems outside render, such as APIs.- The usual fetch pattern is: render, run an Effect, fetch data, then update state.
- Dependency arrays should list the outside values the Effect uses.
- Cleanup matters when a request may finish after the screen changes.
- Many bad Effects disappear once you calculate derived values directly during render.