SyntheticEvent

Event handlers are passed instances of SyntheticEvent, a cross-browser wrapper around the browser's native event. It has the same interface as the browser's native event, including stopPropagation() and preventDefault(), except the events work identically across all browsers.

To register an event handler for the capture phase, append Capture to the event name; for example, instead of using onClick, you would use onClickCapture to handle the click event in the capture phase.

Two implementation details are worth knowing if you've read older React material: as of React 17, event pooling was removed, so SyntheticEvent objects are no longer reused/nulled out after the handler runs (the old e.persist() method is now a no-op, since there's nothing left to persist against). Also as of React 17, React attaches its event listeners to the root DOM container you render into, rather than to the document, which makes it easier to mix multiple React versions or other libraries on the same page.

You can use the currentTarget and relatedTarget to differentiate if the focusing or blurring events originated from outside of the parent element. Here is a demo you can copy and paste that shows how to detect focusing a child, focusing the element itself, and focus entering or leaving the whole subtree (open the console to see the log messages as you tab between the inputs).
RESETRUNFULL
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function Example() {
  return (
    <div
      tabIndex={1}
      onFocus={(e) => {
        if (e.currentTarget === e.target) {
          console.log('focused self');
        } else {
          console.log('focused child', e.target);
        }
        if (!e.currentTarget.contains(e.relatedTarget)) {
          // Not triggered when swapping focus between children
          console.log('focus entered self');
        }
      }}
      onBlur={(e) => {
        if (e.currentTarget === e.target) {
          console.log('unfocused self');
        } else {
          console.log('unfocused child', e.target);
        }
        if (!e.currentTarget.contains(e.relatedTarget)) {
          // Not triggered when swapping focus between children
          console.log('focus left self');
        }
      }}
    >
      <input id="1" />
      <input id="2" />
    </div>
  );
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Example />);
</script>
</body>
</html>