react icon indicating copy to clipboard operation
react copied to clipboard

React onBlur events not firing during unmount

Open taj-codaio opened this issue 7 years ago • 16 comments

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.

taj-codaio avatar Mar 13 '18 17:03 taj-codaio

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.

aweary avatar Mar 27 '18 22:03 aweary

~~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.

jayphelps avatar Apr 16 '18 18:04 jayphelps

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 avatar Apr 26 '18 08:04 jpkempf

@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 avatar Aug 17 '18 18:08 gaearon

@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. :)

jpkempf avatar Aug 22 '18 12:08 jpkempf

same issue on my end

zibra avatar Oct 09 '18 14:10 zibra

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.

aldendaniels avatar Dec 06 '18 15:12 aldendaniels

This is also affecting me

bcbane avatar Sep 24 '19 23:09 bcbane

Any updates?

lukas1994 avatar Feb 29 '20 17:02 lukas1994

ditto

mx2323 avatar May 23 '20 20:05 mx2323

Same here, we’d be happy to contribute towards a fix if there is appetite for one

maltenuhn avatar Jul 03 '20 14:07 maltenuhn

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)} />;
});

Rheeseyb avatar Jul 08 '20 10:07 Rheeseyb

Any progress on this issue?

Aetet avatar Jun 10 '22 12:06 Aetet

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>
    )
  }

bennettdams avatar Oct 25 '22 15:10 bennettdams

The problem still persists here is a snack with React Native

To reproduce:

  1. Use Android or iOS (on Web somehow its working)
  2. Focus the input
  3. Press the button
  4. 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

sanduluca avatar Jun 23 '23 12:06 sanduluca

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.

altyntsevlexus avatar Mar 11 '24 14:03 altyntsevlexus