Search code examples
javascriptreactjsdomonclickdom-events

onClick event.detail is not resetting after React component replaced


The onClick event.detail contains the number of clicks: double-click has event.detail = 2.

When the clicked React component is replaced, the event.detail does not reset. I suspect this is because React has optimized this & kept the same div in the page, so the browser has not reset the event.detail counter.

How can I force this event.detail counter to reset when the component is changed? I want to be able to double-click items in the list multiple times while navigating.

Issue reproduction: https://codesandbox.io/p/sandbox/react-on-click-event-detail-6ndl5v?file=%2Fsrc%2FApp.tsx%3A44%2C12

App.tsx:

const ListItem: React.FC<{
  content: string;
  onClick: (event: React.MouseEvent<HTMLDivElement>) => void;
}> = ({ content, onClick }) => {
  return (
    <div
      onClick={onClick}
      style={{ background: "rgba(0,0,0,0.5)", userSelect: "none" }}
    >
      {content}
    </div>
  );
};

const ListRenderer: React.FC = () => {
  // n tracks the number of double clicks
  const [n, setN] = useState(0);

  // items simulates an updating list of items when double-clicking
  const items = useMemo(
    () => Array.from({ length: 10 }, (_, i) => `${n}: item ${i}`),
    [n]
  );

  const handleClick = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
    console.log(`Click: event.detail: ${event.detail}`);
    if (event.detail === 2) {
      // Double-clicked an item.
      setN((prev) => prev + 1);
    }
  }, []);

  return (
    <>
      <div>
        {items.map((item, index) => (
          <ListItem
            key={`items-${n}-${index}`}
            content={item}
            onClick={handleClick}
          />
        ))}
      </div>
    </>
  );
};


Solution

  • After some experimenting, I've written this React Hook to detect when the event.detail has not reset correctly after a component is re-mounted and to correct the click count accordingly:

    import { useCallback, useRef } from 'react'
    
    // MouseEvent and other events satisfy this.
    interface DetailCountEvent {
      // detail is the number of times the event occurred.
      detail: number
    }
    
    // useDetailCountHandler builds an event handler which correctly resets the
    // event.detail counter when the component is re-mounted.
    //
    // The onClick event.detail contains the number of clicks: double-click has
    // event.detail = 2. When the clicked React component is replaced, the
    // event.detail does not reset.
    //
    // Question: https://stackoverflow.com/q/77719428/431369
    // Issue: https://codesandbox.io/p/sandbox/react-on-click-event-detail-6ndl5v?file=%2Fsrc%2FApp.tsx%3A8%2C23
    // Fix: https://codesandbox.io/p/sandbox/react-on-click-event-detail-possible-fix-4zwk7d?file=%2Fsrc%2FApp.tsx%3A59%2C1
    export function useDetailCountHandler<E extends DetailCountEvent>(
      cb: (e: E, count: number) => void,
    ) {
      const stateRef = useRef({ prev: 0, sub: 0 })
      return useCallback(
        (e: E) => {
          const state = stateRef.current
          let count = e.detail
          if (state.sub >= count) {
            state.sub = 0
          }
          if (state.prev < count - 1) {
            state.sub = count - state.prev - 1
          }
          count -= state.sub
          state.prev = count
          cb(e, count)
        },
        [stateRef, cb],
      )
    }
    

    Usage:

    const handleClick: MouseEventHandler<HTMLDivElement> = 
        useDetailCountHandler(
            useCallback(
                (e, count) => {
                    console.log('onClick', e.detail, count);
                    if (count === 2) {
                        // double click
                    }
                },
                [],
            ),
        );
    
    return <div onClick={handleClick} />;
    

    Using a key to force React to re-create the div unfortunately doesn't reset the event.detail in onClick.

    The above hook is somewhat of a hack but works reliably. If anyone else has a less hacky way to reset event.detail when the component is changed, I would love to know.