Bug: actQueue forever growing in react 19.0.0
Opened this issue · 9 comments
React version: 19.0.0
The current behavior
This is very hard to describe, I encountered a unit test starts to fail when I migrate react from 18.3.1 to 19.0.0.
This is a very large component, in the component tree, there are two things to notice.
- There are usages of
<Suspense />andreact.lazy - There is a component that has
const [ref, setRef] = useState(null)and returns...<div ref={setRef} />...(I need this pattern to append observer + other logic in a effect, ref is a dependency of this effect)
When this component is being tested with react 19.0.0 in react-testing-library, the test enters some kind of infinite loop. I use breakpoint to find that in react-dom-client, the actQueue keeps growing infinitely
To debug this, I started to delete code. I found that
- If I remove the callsite of
setReffrom ref props, the test can finish - If I remove the lazy component and Suspense, the test can finish
if I keep setRef callsite but remove the usage of ref state from the effect, the test still hang
The expected behavior
unit test can finish
TBH I don't expect a fix for it, I currently skip this particular test for the migration.
Post it here just for some discussion
Are you creating a Promise during render? Maybe the React.lazy call is in the Component implementation? That would certainly lead to an infinite loop.
Hi @eps1lon, I can confirm that React.lazy component is created before render
I even simplified that component into something
const MyLazyComponent = React.lazy(async () => {
console.log("before loading")
await Promise.resolve()
console.log("after loading")
return {
default: MyComponent, // MyComponent is statically imported
};
});and I can see the "after loading" is never printed.
Again, this same unit test can pass in react 18.3.1. and have no problem running in real browser (both 18 and 19)
Can you provide a minimal repro? It's unlikely we'll be able to fix it without a repro.
@jzhan-canva I have also tried to upgrade to React 19 without success due to this issue. It very clearly works when the lazy component is removed but appears to eat RAM to the point of crashing when the lazy component is in the DOM tree.
Have you managed to find any workarounds to this as yet?
@copperseed do you have a minimal reproducible example for react team? I don't reproduce my case in browser (my case only happen in testing env + jsdom)
@jzhan-canva no. I also only experience it in the testing environment. No issues in the browser.
You need to break the cycle by not triggering a state update directly from the ref prop. The recommended pattern for observing a DOM node is to use useRef combined with useEffect.
Primary Solution: Use useRef + useEffect
This is the most idiomatic and robust way to handle this scenario in modern React. It avoids causing re-renders just to get a reference to a DOM node.
Replace useState
// Before
const [ref, setRef] = useState(null);
useEffect(() => {
if (ref) {
// ... logic with the observer
}
}, [ref]);
return
With useRef
import React, { useRef, useEffect } from 'react';
// ...
const ref = useRef(null); // 1. Create a stable ref object.
useEffect(() => {
const element = ref.current; // 2. Access the node inside useEffect.
if (element) {
// ... your logic here (e.g., new IntersectionObserver(...,)).
// The observer will be set up once the element is mounted.
}
// Optional: Cleanup function to disconnect the observer
return () => {
// ... your cleanup logic
};
}, []); // 3. Use an empty dependency array to run this effect only once on mount.
return
htt