Skip to content
question

useEffect fetch running twice in my take-home test?? is my code broken

Jade Thompson@jade_learns

October 7, 2026 · 1 min read

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

Sneha Kulkarni#7 in Rerender Anonymous11 likes

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.

View in the thread

Get the best of #rerender-anonymous weekly

One email each Monday with the most useful posts. No spam; unsubscribe in one click.

Comments

8

Sign in to join the conversation.

Sign in to comment
Rhys Llewellyn@rhys_teaches_code#10 in Rerender Anonymous
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 😄
2 net votes
Jade Thompson@jade_learns#1 in Rerender Anonymous
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
0 net votes

Accepted answer

Sneha Kulkarni@laidoff_not_out#7 in Rerender Anonymous
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.
11 net votes
Jade Thompson@jade_learns#1 in Rerender Anonymous
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 💪
2 net votes
Sneha Kulkarni@laidoff_not_out#7 in Rerender Anonymous
fingers crossed. and they love 'why does it run twice' as a follow-up question, so you're now ready for that too
2 net votes
Kuya Migs@kuya_migs_codes3 rep in Rerender Anonymous
Classic! May video ako about this lods, pero Sneha's answer is cleaner 🔥 good luck sa take-home Jade 💪
2 net votes
Aoife Brennan@aoifeb_dev1 rep in Rerender Anonymous
jade if you get round 3 before me i'll be raging (i'm delighted for you)
4 net votes
Jade Thompson@jade_learns#1 in Rerender Anonymous
aoife we said whoever gets an offer first buys the cake. terms and conditions apply 😂
3 net votes