React version: 19.2.5
Steps To Reproduce
- Create React app using create-vite
- Remove
StrictMode from main.tsx (just to reduce the number of debug logs)
- Replace code in
App.tsx
- Run
npm dev
- Open browser
- Click the "Close" button
- Click the "Open" button
Code example:
import {type ReactNode, useState, useEffect} from 'react'
import './App.css'
function Internal(props: { opened: boolean, children: ReactNode }) {
const {opened: externalOpened} = props
const [opened, setOpened] = useState(externalOpened)
const [prevOpened, setPrevOpened] = useState(externalOpened)
console.log('render', { opened, externalOpened })
if (externalOpened !== prevOpened) {
setPrevOpened(externalOpened)
console.log('externalOpened !== prevOpened', externalOpened)
setOpened(externalOpened)
}
useEffect(() => {
const timeout = setTimeout(() => {
if (!externalOpened) {
console.log('useEffect', false)
setOpened(false)
}
}, 300)
return () => clearTimeout(timeout);
}, [externalOpened])
return opened ? <>{props.children}</> : null
}
function App() {
const [opened, setOpened] = useState(true)
return (
<>
<button onClick={() => {
console.log('open')
setOpened(true)
}}>Open</button>
<Internal opened={opened}>
<button onClick={() => {
console.log('close')
setOpened(false)
}}>Close</button>
</Internal></>
)
}
export default App
The current behavior
When you click on the "Open" button after closing, the panel from Internal component is shown and is hidden immediately after showing.
Logs in the browser console:
App.tsx:10 render {opened: true, externalOpened: true}
15:41:27.506 "---"
15:41:27.509 '---'
15:41:30.045 App.tsx:44 close
15:41:30.047 App.tsx:10 render {opened: true, externalOpened: false}
15:41:30.047 App.tsx:15 externalOpened !== prevOpened false
15:41:30.047 App.tsx:10 render {opened: false, externalOpened: false}
15:41:30.350 App.tsx:22 useEffect false
15:41:32.597 "---"
15:41:32.604 '---'
15:41:34.350 App.tsx:39 open
15:41:34.352 App.tsx:10 render {opened: false, externalOpened: true}
15:41:34.352 App.tsx:15 externalOpened !== prevOpened true
15:41:34.352 App.tsx:10 render {opened: true, externalOpened: true}
15:41:34.355 App.tsx:10 render {opened: false, externalOpened: true}
The expected behavior
When you click on the "Open" button after closing, the panel from Internal component should be just shown.
Notes
The main question for me is why the last render happens and why the some outdated state is used? I provided timestamps to see that the last render can't be triggered by planed setTimeout or something like this. I tried to investigate the problem. I found some information about such a thing as state bailing out, but all issues I found are closed with a note that bailing out should not affect render logic or state manipulations. What I observe from the logs:
- When I press the "Close" button, the effect is triggered. However, at the moment when timeout is executed, both
App and Internal components are in closed state, so as far as I understand, React bails out update from useTimeout, looks reasonable, no questions here.
- Then, when I press the "Open" button, for some reason I see the last render. It seems like a reaction to bail out
setState from previously triggered setTimout, so finally Internal component is rendered in closed state even though App component in open state. If I am removing useEffect block the problem disappears.
Possible questions
- "Why is the provided code so complicated and strange?" - It minimal reproducible example where a problem appears. Yes, it is abstract, but I faced a real problem in production code, where there was an attempt to sync internal and external states and perform some synchronizations on animation state (
setTimiout is some emulation of transitionend event)
- "Why
useEffect hook is needed here?" - Again, it is examples, real code is more complex and useEffect it is emulation of synchronizing component state with animation events.
React version: 19.2.5
Steps To Reproduce
StrictModefrommain.tsx(just to reduce the number of debug logs)App.tsxnpm devCode example:
The current behavior
When you click on the "Open" button after closing, the panel from
Internalcomponent is shown and is hidden immediately after showing.Logs in the browser console:
The expected behavior
When you click on the "Open" button after closing, the panel from
Internalcomponent should be just shown.Notes
The main question for me is why the last render happens and why the some outdated state is used? I provided timestamps to see that the last render can't be triggered by planed
setTimeoutor something like this. I tried to investigate the problem. I found some information about such a thing as state bailing out, but all issues I found are closed with a note that bailing out should not affect render logic or state manipulations. What I observe from the logs:AppandInternalcomponents are in closed state, so as far as I understand, React bails out update fromuseTimeout, looks reasonable, no questions here.setStatefrom previously triggeredsetTimout, so finallyInternalcomponent is rendered in closed state even thoughAppcomponent in open state. If I am removinguseEffectblock the problem disappears.Possible questions
setTimioutis some emulation oftransitionendevent)useEffecthook is needed here?" - Again, it is examples, real code is more complex anduseEffectit is emulation of synchronizing component state with animation events.