Skip to content

Resium Event Listener Bug: cesiumEventProps Don't Work in React Strict ModeΒ #736

@groovyjovy

Description

@groovyjovy

Summary

Event handlers defined via cesiumEventProps (such as onMoveEnd, onMoveStart, onChange) do not work correctly in React Strict Mode. After the Strict Mode remount cycle, event listeners are not re-registered, causing events to silently fail.

Environment

  • resium: 1.19.2
  • React: 19.x (with Strict Mode enabled)
  • Tested on: macOS, Chrome

Reproduction Steps

  1. Create a React app with Strict Mode enabled (default in Create React App / Vite)
  2. Add a Camera component with an event handler:
import { Viewer, Camera } from 'resium';

function App() {
  return (
    <Viewer>
      <Camera onMoveEnd={() => console.log('Camera moved!')} />
    </Viewer>
  );
}
  1. Move the camera by dragging the map
  2. Expected: "Camera moved!" appears in the console
  3. Actual: Nothing appears in the console

Root Cause Analysis

React Strict Mode Behavior

In development, React Strict Mode intentionally double-invokes lifecycle methods:

mount β†’ unmount β†’ mount (second time)

The Bug in hooks.ts

In the unmount function (around line 270), attachedEvents.current is cleared but prevProps.current is not:

// Detach all events
if (element.current && !isDestroyed(element.current)) {
  const attachedEventKeys = Object.keys(attachedEvents.current);
  for (const k of attachedEventKeys) {
    const eventHandler = element.current[k];
    eventHandler?.removeEventListener?.(attachedEvents.current[k]);
  }
}

attachedEvents.current = {};  // βœ“ Cleared
// prevProps.current is NOT cleared  // βœ— BUG
provided.current = undefined;
stateRef.current = undefined;
element.current = undefined;

Why This Causes the Bug

The updateProperties function calculates prop differences by comparing props with prevProps.current:

const propDiff = propsKeys
  .concat(
    Object.keys(prevProps.current).filter(k => !propsKeys.includes(k)),
  )
  .filter(k => prevProps.current[k] !== props[k])  // Same reference = no diff
  .map(k => [k, prevProps.current[k], props[k]]);

When useCallback is used (or a stable function reference), prevProps.current['onMoveEnd'] === props['onMoveEnd'] evaluates to true, so no difference is detected and the event listener is never re-added.

Execution Flow

Step prevProps.current attachedEvents.current Result
1st mount {} {} propDiff=['onMoveEnd'] β†’ Listener added βœ“
unmount {onMoveEnd: fn} (kept) {} (cleared) Listener removed
2nd mount {onMoveEnd: fn} (stale) {} propDiff=[] (no diff) β†’ Listener NOT added βœ—

Proposed Fix

Add one line to clear prevProps.current in the unmount function:

// hooks.ts, in the unmount function (around line 267-270)
attachedEvents.current = {};
// Fix: Clear prevProps on unmount to ensure proper event re-registration
// when component remounts (e.g., in React Strict Mode)
prevProps.current = {} as Props;  // ← ADD THIS LINE
provided.current = undefined;
stateRef.current = undefined;
element.current = undefined;

After Fix

Step prevProps.current attachedEvents.current Result
1st mount {} {} propDiff=['onMoveEnd'] β†’ Listener added βœ“
unmount {} (cleared) {} (cleared) Listener removed
2nd mount {} {} propDiff=['onMoveEnd'] β†’ Listener added βœ“

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions