State updates outside of `act` interfere with subsequent state updates during `act`
See original GitHub issueA state update to a component scheduled outside of an act()
callback can prevent a state update scheduled inside of an act()
callback from executing synchronously as it should:
Steps to reproduce:
import { createElement, render } from "preact";
import { useState, useEffect } from "preact/hooks";
import { act } from "preact/test-utils";
function App({ foo }) {
const [fooState, setFooState] = useState(null);
useEffect(() => {
setFooState(foo);
}, [foo, setFooState]);
return <div>{fooState}</div>;
}
const container = document.querySelector("#app");
render(<App foo={1} />, container);
act(() => {
render(<App foo={2} />, container);
});
console.log("initial render output", container.innerHTML);
Runnable demo: https://codesandbox.io/s/preact-10-state-update-after-unmount-y1fj0
nb. With this sandbox I see different outputs in the DOM when viewing the sandbox output in an iframe vs a new window. In the iframe the DOM output matches the console log. In a new window, the DOM updates after the console log to show “2”.
Expected output:
"initial render output <div>2</div>"
Actual output:
"initial render output <div>1</div>"
Notes:
When setState
is called or a state update hook runs, the enqueueRender
function is called. This function uses the options.debounceRendering
hook which is overridden in the context of an act
call. However, options.debounceRendering
is only called if the deferred rendering queue is empty. In the above example, when the component is updated inside the act
call, the queue is non-empty so rendering is deferred.
The above example was extracted from a test where a state update was being triggered in a timeout, outside of an act
call, to work around an issue with CSS transitions in certain browsers. This surprisingly interfered with later tests due to the global deferred render queue.
Issue Analytics
- State:
- Created 4 years ago
- Comments:6 (6 by maintainers)
Top GitHub Comments
In the context that I extracted this test case from, the state update is triggered asynchronously inside a
useEffect
handler usingsetTimeout
, so it is difficult to wrap inact
. I’m sure I could figure out something for that specific test. The bigger issue though is that this issue creates a footgun which is very confusing to debug because something that one test does can affect what happens in later, unrelated tests, due to the global queues used for deferred rendering.On reflection I realized that there was a more general issue here to do with the way the scheduling hooks (
options.{debounceRendering, requestAnimationFrame}
) are handled that could break other code that temporarily changes these hooks for any reason.