useEffect fetch running twice in my take-home test?? is my code broken
Jade Thompson@jade_learns
doing a take-home for a junior role (round 2, we move 💪). simple React app that fetches a list of products. in the network tab i can see the request going out TWICE every time the page loads.
i panicked and added a flag so it only fetches once, and now sometimes the list doesn't show at all, which is worse lol. here's the code:
import { useEffect, useState } from 'react'
let hasFetched = false
export default function Products() {
const [products, setProducts] = useState([])
useEffect(() => {
if (hasFetched) return
hasFetched = true
fetch('/api/products')
.then((res) => res.json())
.then((data) => setProducts(data))
}, [])
return (
<ul>
{products.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
)
}
vite + react 19. is this a bug in my code or is react being weird? deadline is friday, any help proper appreciated 🙏
Accepted answer
rhys is right. in development, StrictMode mounts your component, unmounts it, and mounts it again, on purpose, to catch effects that don't clean up after themselves. so the effect runs twice in dev only. production runs it once.
why your flag breaks things: hasFetched lives outside the component, at module level. it never resets. first mount sets it true, the fake unmount/remount sees true and skips, and if you navigate away and come back later the component mounts fresh with an empty list and never fetches again. empty list. that's your "sometimes nothing".
the fix is not to stop the second run, it's to clean up the first one:
useEffect(() => {
const controller = new AbortController()
fetch('/api/products', { signal: controller.signal })
.then((res) => res.json())
.then(setProducts)
.catch((err) => {
if (err.name !== 'AbortError') setError(err)
})
return () => controller.abort()
}, [])
in dev you'll now see the first request cancelled and the second one complete. delete the flag.
and put one line in your README: "effects run twice in dev because of StrictMode, handled with an AbortController cleanup". reviewers love seeing that you know why. in a real app you'd probably use TanStack Query or your framework's loader, but for a take-home this is exactly right.
Get the best of #rerender-anonymous weekly
One email each Monday with the most useful posts. No spam; unsubscribe in one click.
Comments
Sign in to join the conversation.
Sign in to commentRhys Llewellyn@rhys_teaches_code
Not a bug, I think you've just met StrictMode! Someone who writes React for a living will give you the full answer, but a quick check for you first: does it still happen after npm run build and npm run preview? That's your homework 😄
Jade Thompson@jade_learns
ok ran build + preview and it only fetches once?? so it IS react being weird lol. but why does my flag version sometimes show nothing
Accepted answer
Sneha Kulkarni@laidoff_not_out
rhys is right. in development, StrictMode mounts your component, unmounts it, and mounts it again, on purpose, to catch effects that don't clean up after themselves. so the effect runs twice in dev only. production runs it once.
why your flag breaks things: hasFetched lives outside the component, at module level. it never resets. first mount sets it true, the fake unmount/remount sees true and skips, and if you navigate away and come back later the component mounts fresh with an empty list and never fetches again. empty list. that's your "sometimes nothing".
the fix is not to stop the second run, it's to clean up the first one:
useEffect(() => {
const controller = new AbortController()
fetch('/api/products', { signal: controller.signal })
.then((res) => res.json())
.then(setProducts)
.catch((err) => {
if (err.name !== 'AbortError') setError(err)
})
return () => controller.abort()
}, [])
in dev you'll now see the first request cancelled and the second one complete. delete the flag.
and put one line in your README: "effects run twice in dev because of StrictMode, handled with an AbortController cleanup". reviewers love seeing that you know why. in a real app you'd probably use TanStack Query or your framework's loader, but for a take-home this is exactly right.
Jade Thompson@jade_learns
this is amazing thank you sneha. AbortController version works, flag deleted, and the StrictMode note is in the readme. if i get this job i owe you cake 🎂 we move 💪
Sneha Kulkarni@laidoff_not_out
fingers crossed. and they love 'why does it run twice' as a follow-up question, so you're now ready for that too
Kuya Migs@kuya_migs_codes
Classic! May video ako about this lods, pero Sneha's answer is cleaner 🔥 good luck sa take-home Jade 💪
Jade Thompson@jade_learns
aoife we said whoever gets an offer first buys the cake. terms and conditions apply 😂