How to fix useEffect infinite loop caused by dependency array

My component is stuck in an infinite loop. The useEffect fires, updates state, the component re-renders, and the useEffect fires again:

function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState(null)
  const filters = { active: true } // created on every render

  useEffect(() => {
    fetchUser(userId, filters).then(setUser)
  }, [userId, filters]) // filters is a new object every render!

  return <div>{user?.name}</div>
}

The browser freezes and the network tab shows hundreds of requests. How do I stop this?

Solution

The problem is that filters is an object created inside the component body. JavaScript compares objects by reference, so { active: true } !== { active: true } on every render. React sees a new value in the dependency array and re-runs the effect.

If the value is static, move it outside the component so it is created only once:

const filters = { active: true } // defined once, outside the component

function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState(null)

  useEffect(() => {
    fetchUser(userId, filters).then(setUser)
  }, [userId])

  return <div>{user?.name}</div>
}

If the value depends on props or state, use useMemo to keep the reference stable:

function UserProfile({ userId, isActive }: { userId: string; isActive: boolean }) {
  const [user, setUser] = useState(null)

  const filters = useMemo(() => ({ active: isActive }), [isActive])

  useEffect(() => {
    fetchUser(userId, filters).then(setUser)
  }, [userId, filters])

  return <div>{user?.name}</div>
}
Alternative #1

If your dependency is a function rather than an object, the same problem applies. Functions are also compared by reference. Wrap them with useCallback to keep the reference stable across renders:

function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState(null)

  const fetchData = useCallback(async () => {
    const data = await fetchUser(userId)
    setUser(data)
  }, [userId])

  useEffect(() => {
    fetchData()
  }, [fetchData])

  return <div>{user?.name}</div>
}

Without useCallback, fetchData is a new function on every render, causing the same loop.

Alternative #2

Sometimes the loop is caused by setting state inside useEffect when the state value itself is in the dependency array. Use the functional updater form of setState so you do not need to include the old value in the deps:

// Broken: data is in deps, effect updates data -> loop
useEffect(() => {
  setData(transform(data))
}, [data])

// Fixed: reads previous data without adding it to deps
useEffect(() => {
  setData((prev) => transform(prev))
}, [])

Or better yet, derive the value during render with useMemo and avoid the effect entirely:

const transformedData = useMemo(() => transform(data), [data])
Alternative #3

If you cannot memoize a callback prop because it changes on every render from the parent, hold its latest version in a ref. The ref update does not trigger re-renders, so the effect runs only once:

function UserProfile({ onLoad }: { onLoad: (user: User) => void }) {
  const [user, setUser] = useState<User | null>(null)
  const onLoadRef = useRef(onLoad)

  useEffect(() => {
    onLoadRef.current = onLoad
  })

  useEffect(() => {
    fetchUser().then((data) => {
      setUser(data)
      onLoadRef.current(data)
    })
  }, [])
}
Last modified: April 16, 2026
Stay in the loop
Subscribe to our newsletter to get the latest articles delivered to your inbox