react
react copied to clipboard
React onBlur events not firing during unmount
Do you want to request a feature or report a bug?
bug
What is the current behavior?
If a DOM element rendered by a React component has focus, and the React component unmounts, the React onBlur event does not fire on parent DOM elements.
If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React. Paste the link to your JSFiddle (https://jsfiddle.net/Luktwrdm/) or CodeSandbox (https://codesandbox.io/s/new) example below:
https://codesandbox.io/s/134wrzy6q7
What is the expected behavior?
I would expect that, just like the browser fires a focusout event when removing a DOM node, React would fire an onBlur events up to parent nodes when the focused node is removed / unmounted.
Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
React: 16.2 Mac OS X: 10.13.2 Browser: Chrome 67.0.3366.0, Chrome 64.0.3282.186
No idea if this worked in earlier versions of React.
I suspect this is a result of how React handles events that occur during reconciliation. Currently, React disables the synthetic event system before committing. That means events that occur as a result of node removal aren't actually handled.
We've talked about having an event queue that gets processed after the commit phase, but right now there's no current plan to do that.
~~Sounds like rather than onBlur bubbling we'd want focusout support via onFocusOut since browsers don't bubble native blur events and doing so in React could cause bugs by breaking those expectations.~~ focusout is not supported yet though for various reasons: https://github.com/facebook/react/issues/6410
Today I learned that React actually already bubbles blur events, unlike browsers, which makes me sad 😢Now I understand why this issue was created.
I have noticed similar behaviour with the onChange event. I'm not sure if it merits a separate issue, so I'm posting a response here first. I'm happy to move my comment to a new issue, if need be.
Here's a reduced test case (open the console and click around a bit): https://codesandbox.io/s/84wz9wlvxl
In this scenario, once you check one of the boxes, it's place in the virtual DOM changes. As a consequence of that, the change event gets lost while the click event bubbles up to the document. If I have understood @aweary correctly, this is currently the intended behaviour from the point of view of React, yes?
@jpkempf A separate issue is best for something that doesn't look like what's being described by this issue's title. Because otherwise it's unsearchable. Thanks.
@gaearon sure, no problem! I've created https://github.com/facebook/react/issues/13459. This is my first issue so I hope everything's fine. :)
same issue on my end
Also hitting this - and another simple repro case: https://codepen.io/anon/pen/NEQbjM?editors=1112
Here you can see that the native focusout event does fire, but there's no corresponding synthetic onBlur event.
This is also affecting me
Any updates?
ditto
Same here, we’d be happy to contribute towards a fix if there is appetite for one
For those still hitting this, we came up with a workaround here: https://codesandbox.io/s/determined-euler-t7ijw?file=/src/App.js In a nut shell, what we did was use a wrapper which:
- Attaches the
onBlurevent handler directly to the underlying DOM element via auseEffect, requiring attaching a ref to theinputitself - Uses a
forwardRefcombined with compose-react-refs to ensure we can still pass through a ref and also have access to a ref inside the wrapper - Uses a ref to track the previous event handler for cleanup (we couldn't rely on the
useEffectclean up to do this because I believe that happens during the reconciliation, meaning the event handler is removed before being called in this use case - I have verified this)
There is one major caveat here though - because it requires attaching an event handler to the underlying element, it means that handler will be dealing with the raw event rather than React's SyntheticEvent, which may be a deal breaker for you.
To save you clicking the link, here's the code for the wrapper (which could equally have been written as a HOC):
import React from "react";
import composeRefs from "@seznam/compose-react-refs";
const WrappedInput = React.forwardRef((props, outerRef) => {
const { onBlur, ...inputProps } = props;
const innerRef = React.useRef(null);
const lastOnBlur = React.useRef(null);
React.useEffect(() => {
if (innerRef != null && innerRef.current != null && onBlur != null) {
if (lastOnBlur.current != null) {
innerRef.current.removeEventListener("blur", lastOnBlur.current);
}
innerRef.current.addEventListener("blur", onBlur);
lastOnBlur.current = onBlur;
}
}, [innerRef, onBlur]);
return <input {...inputProps} ref={composeRefs(outerRef, innerRef)} />;
});
Any progress on this issue?
Another workaround for conditional rendering
I wanted to submit a form via onBlur and encountered this bug. In my case, I was conditionally rendering either the data to display or a form to edit that data in the same place.
Something like:
function Post({ isPostEditMode }: { isPostEditMode: boolean }): JSX.Element {
return (
<div>
<p>Post</p>
{isPostEditMode ? (
// Form
<form onBlur={...}>...</form>
) : (
// Data
<div>Data</div>
)}
</div>
)
}
Not sure if obvious for everyone, but there is another workaround:
Don't render the elements conditionally, but apply the display: none CSS property to either one of them. That way, the elements won't get re-/mounted and hence the onBlur callback is working as expected:
// example uses Tailwind
function Post({ isPostEditMode }: { isPostEditMode: boolean }): JSX.Element {
return (
<div>
<p>Post</p>
{/* Form */}
<form
className={isPostEditMode ? 'hidden' : 'block'}
onBlur={...}
>
...
</form>
{/* Data */}
<div className={isPostEditMode ? 'block' : 'hidden'}>Data</div>
</div>
)
}
The problem still persists here is a snack with React Native
To reproduce:
- Use Android or iOS (on Web somehow its working)
- Focus the input
- Press the button
- The text will say that the input is focused (because onBlur was not called)
https://snack.expo.dev/@sanduluca/react-onblur-events-not-firing-during-unmount
Here is a possible workaround. In my case I have a button that unmount component with input inside. So we can check an active element and trigger blur() before unmount.
const handleClose = () = > {
const activeElement = document.activeElement;
if (activeElement instanceof HTMLElement) {
activeElement.blur();
}
close();
}
For proper usage we need to check if the active element is an HTMLElement, not just Element.