React: React onBlur events not firing during unmount

Created on 13 Mar 2018  路  12Comments  路  Source: facebook/react

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.

DOM Bug Needs Investigation

All 12 comments

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鈥檇 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:

  1. Attaches the onBlur event handler directly to the underlying DOM element via a useEffect, requiring attaching a ref to the input itself
  2. Uses a forwardRef combined with compose-react-refs to ensure we can still pass through a ref and also have access to a ref inside the wrapper
  3. Uses a ref to track the previous event handler for cleanup (we couldn't rely on the useEffect clean 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)} />;
});
Was this page helpful?
0 / 5 - 0 ratings