React version: 16.12.0
<button/> HTML element with onFocus event handler defined<button ref={btnRef} onFocus={() => console.log("I'm in focus")}>Focus Target</button>
useEffect cleanup code which will call focus()React.useEffect(() => {
const ref = btnRef.current; // btnRef is a ref to the button component above
return () => {
console.log("effect cleanup ", new Date().toISOString());
ref.focus();
};
}, []);
When the cleanup code from useEffect is called, onFocus event handler of the button is not called. The button is rendered in a component which is mounted when useEffect cleanup executes. For example it can be in the parent component.
Link to code example: https://codesandbox.io/s/onfocus-not-called-from-useeffect-cleanup-npw7k
onFocus event handleronFocus event handleronFocus is not called
onFocus is called
It also seems that this is only broken when the effect is unmounting, but works fine when it is updating:
// Helper component to trigger a function when either k changes, or it is unmounted
function OnUnmount({ fn, k }) {
React.useEffect(() => {
return () => {
fn();
};
}, [k])
return null;
}
export default function App() {
const inputRef = React.useRef();
const [state, setState] = React.useState(0);
function run() {
setState(s => s + 1);
}
return (
<div>
<button onClick={run}>Run</button>
<OnUnmount fn={() => {
inputRef.current.focus();
}} key={state}/> //
<input ref={inputRef} onFocus={() => {
console.log('onFocus');
}}/>
</div>
);
}
In this example the input isn't even focused at all, but if you change key={state} to k={state} it works fine, although there is seemingly no reason for such a discrepancy.
It goes without saying that adding setTimeout(0) fixes things.
The problem is a bit wider than that: any dom events triggered using ref will not work, and the reason is the following:
1)When the DOM modification happens, such as on unmount of the buttons from the example, eventually commitRootImpl is being called. This is where React applies DOM modifications.
2)As part of the prepareForCommit, React disables browser event emitter.
3)The cleanup function returned from the callback of useEffect hook triggers browser focus event using ref, and if you attach any non-React event listeners to the element it will be triggered, but React event is not being dispatched because event emitter is not enabled. Thus, React suppresses focus event.
4)Then, React enables event emitter back in resetAfterCommit, but its too late, because the focus event is already lost.
As a result, you see that the element is in focus in your browser, but proper React callback was not called. And, it explains why setTimeout(0) works.
I hope my investigation will be helpful to React core team, and I'm ready to help with the solution implementation, once there is an agreement about what to do about it.
This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!
bump
It seems like the original intent was to suppress events caused by DOM mutations but because cleanup fires synchronously, this suppresses the focus too.
However, we have already changed the behavior in master to fire useEffect cleanup asynchronously, which accidentally fixes this issue (to be included in React 17). I think it would still be relevant for synchronous cleanup (such as useLayoutEffect) but I'm not sure if we're going to fix that case. I'll keep it open for more discussion.
Hey all,
I think I have stumbled on the same bug but also discovered another weird behaviour in relation to this.
Both issues I'm seeing here (the event one already mentioned, + the new one) appear to have been fixed in v17.
That said, I thought I'd report it anyway because of the second behavior I saw, as I don't know if the react team is aware.
I have setup a sandbox to demonstrate the issue.
onFocusCapture listener.focus listener (capture phase too) on the document.https://codesandbox.io/s/react-unmount-focus-bug-0qn8q
Here's what we can observe:
onFocusCapture event was never called (same issue as discussed in this thread I think), we only see the native one.
Here is the exact same sandbox, but with react updated to v17, you can see both issues seem to be resolved:
https://codesandbox.io/s/react-unmount-focus-bug-fixed-in-v17-t7zcw

Let me know if that weird re-focusing behavior was known or not. Happy to see if fixed in 17 but wasn't sure if it was accidental like @gaearon mentioned.
鉁岋笍
Most helpful comment
Hey all,
I think I have stumbled on the same bug but also discovered another weird behaviour in relation to this.
Both issues I'm seeing here (the event one already mentioned, + the new one) appear to have been fixed in v17.
That said, I thought I'd report it anyway because of the second behavior I saw, as I don't know if the react team is aware.
I have setup a sandbox to demonstrate the issue.
onFocusCapturelistener.focuslistener (capture phase too) on the document.latest stable react 16.13.1
https://codesandbox.io/s/react-unmount-focus-bug-0qn8q
Here's what we can observe:
onFocusCaptureevent was never called (same issue as discussed in this thread I think), we only see the native one.react 17.0.0-rc.3
Here is the exact same sandbox, but with react updated to v17, you can see both issues seem to be resolved:
https://codesandbox.io/s/react-unmount-focus-bug-fixed-in-v17-t7zcw

Let me know if that weird re-focusing behavior was known or not. Happy to see if fixed in 17 but wasn't sure if it was accidental like @gaearon mentioned.
鉁岋笍