Compared to React 16, the new features of React17 are featureless, so the upgrade from 16 to 17 is relatively smooth. One disruptive change, however, is the change in the event system.

In React 17, React will no longer attach event handlers at the document level. Instead, it will attach them to the root DOM container into which your React tree is rendered.

Exceptions are thrown wherever document.addEventListener is used in the project. The official solution is to change the event behavior to the useCapture stage

1
document.addEventListener('click', onClick, { capture: true });

This method solves most of the problems, but at some point there will still be some issues.

Since the administrative control of the event is no longer associated with React, the logic to block bubbling behavior, for example, is not implemented when there are other elements in the page component that have event listening and co-location logic.

A simple example of the main code that displays a popover and closes it when an external element is clicked.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
const TipTest = () => {
  const [visible, setVisible] = React.useState(false);
    const toggleTip = () => setVisible(!visible);
  const popClick = (e) => {
    // 阻止继续向上冒泡
    e.nativeEvent.stopImmediatePropagation();
  };
 
  React.useLayoutEffect(() => {
    const hideTip = () => setVisible(false);
    document.addEventListener('click', hideTip, { capture: true });
    return () => document.removeEventListener('click', hideTip, { capture: true });
  });
 
  return (
    <div>
      <button onClick={toggleTip}/>TIPS</button>
      <Tip onClick={popClick} visible={visible}>
    </div>
  );
}

The logic in the above example is fine in React 16, it’s a tricky way to use the event bubbling mechanism. But in React 17, since the stopImmediatePropagation in popClick does not prevent the event behavior to document, an exception occurs where every button click also triggers hideTip, causing the Tip capability to fail.

There is no better solution to this situation. In order to be compatible with the existing logic and not make extensive changes, a compromise is to migrate the event listener of document.addEventListener to the root node and implement an event proxy to replace document.addEventListener to minimize changes. How can this be done?

First, implement a helper tool object to replace document.addEventListener.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
type EventNames = 'click';
 
const emiter = new EventEmitter();
 
export const rootNodeEventHelper = {
    addEventListener(eventName: EventNames, callback: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void, _options?) {
        emiter.on(eventName, callback);
    },
    removeEventListener(eventName: EventNames, callback: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void, _options?) {
        emiter.off(eventName, callback);
    },
    emit(eventName: EventNames, event: React.MouseEvent<HTMLDivElement, MouseEvent>) {
        emiter.emit(eventName, event);
    },
    once(eventName: EventNames, callback: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void, context?) {
        emiter.once(eventName, callback, context);
    },
}

Then, register and bind the relevant events on the root App component.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
export default class App extends Component {
    onClick: React.MouseEventHandler<HTMLDivElement> = (event) => {
        rootNodeEventHelper.emit('click', event);
    }
    render() {
        return (
            <ErrorBoundary name="App">
                <div className="FramelessApp" onClick={this.onClick}>
                    <TitleBar />
                    <App />
                </div>
            </ErrorBoundary>
        );
    }
}

Finally, replace document. in the previous example with rootNodeEventHelper., so that the logic within the component is consistent with that in React16.

Of course, for the specific scenario in the example, we can also modify the logic to add recognition and handling of clicked nodes to the hideTip method. This is a slightly more tedious but more conventional approach. Example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
const TipTest = () => {
  const [visible, setVisible] = React.useState(false);
  const toggleTip = () => setVisible(!visible);
  const popRef = React.useRef();
 
  React.useLayoutEffect(() => {
    const hideTip = (e) => {
      if (e.path.includes(findDomNode(poRef.current))) return;
      setVisible(false);
    };
 
    document.addEventListener('click', hideTip, { capture: true });
    return () => document.removeEventListener('click', hideTip, { capture: true });
  });
 
  return (
    <div>
      <button onClick={toggleTip}/>TIPS</button>
      <Popover ref={popRef} onClick={popClick} visible={visible}>
    </div>
  );
}