Search code examples
reactjsmapbox-gl-jsreact-refreact-forwardrefmapbox-gl-draw

React forwardRef and callback ref, ref becomes undefined


This is not a Mapbox related issue, and you don’t need any map or related stuff knowledge to help. My issue is related to React refs.

I’m using react-map-gl (Mapbox for React) with @mapbox/mapbox-gl-draw that allow user to draw geometries on the map. This app has 2 components: DrawControl that exposes draw features and AllPastures that renders the map and manage create/update/delete geometries drawed.

When user draws a polygon it fires the DrawControl method onCreate that executes onCreateOrUpdate. I’m passing the Draw by forwardRef from DrawControl to AllPastures.

This is a DrawControl component:

export const DrawControl = React.forwardRef<MapboxDraw, DrawControlProps>(
  (props: DrawControlProps, ref) => {
    const drawRef: MapboxDraw = useControl<MapboxDraw>(
      () => new MapboxDraw(props),
      ({ map }) => {
        map.on('draw.create', props.onCreate);
        map.on('draw.update', props.onUpdate);
        map.on('draw.delete', props.onDelete);
        map.on('draw.modechange', props.onModeChange);
      },
      ({ map }) => {
        map.off('draw.create', props.onCreate);
        map.off('draw.update', props.onUpdate);
        map.off('draw.delete', props.onDelete);
        map.off('draw.modechange', props.onModeChange);
      },
      {
        position: props.position,
      }
    );

    // exposing drawRef outside the component by Ref
    React.useImperativeHandle(ref, () => drawRef, [drawRef]);

    return null;
  }

and this is part of AllPastures component:

export const AllPastures = () => {
  ...
  const [drawRef, setDrawRef] = React.useState<MapboxDraw>();

  // here I’m using callback ref to react to drawRef changes
  const onDrawRef = React.useCallback((ref: MapboxDraw | null) => {
    if (ref) {
      setDrawRef(ref);
    }
  }, []);

  React.useEffect(() => {
    // it's ok here, drawRef is not undefined
    console.log('useEffect drawRef when app is loading', drawRef);
  }, [map, drawRef]);

  const onCreateOrUpdate = (e: { features: Feature[] }) => {
    // Why drawRef is undefined here????????????????????
    console.log('drawRef under onCreateOrUpdate method', drawRef);
  };

  ...

  return (
      <DrawControl
        ref={onDrawRef}
        position="top-right"
        displayControlsDefault={false}
        controls={{
          polygon: true,
          trash: true,
        }}
        defaultMode="simple_select"
        onCreate={onCreateOrUpdate}
        onUpdate={onCreateOrUpdate}
      />
  );
};

My issue is, discover why when the the method onCreateOrUpdate the drawRef is undefined?


Here is a related sandbox simulating the issue: https://stackblitz.com/edit/vitejs-vite-bvutvb?file=src%2FAllPastures.tsx

Just draw any polygon on the map and check the console.log drawRef under onCreateOrUpdate method is undefined.

Please, after make changes in the code, do a F5 to complete refresh the page and test it again.


Solution

  • For someone else is looking for a solution, I found a way forwarding the drawRef outside the DrawControl using forwardRef, useImperativeHandle and attaching that ref to my <DrawControl>.

    The working example bellow, and a complete solution on: https://stackblitz.com/edit/vitejs-vite-bvutvb?file=src%2FDraw.tsx

    export const Draw = () => {
      const { current: map } = useMap();
      const drawRef = React.useRef<MapboxDraw>(null);
    
      if (!map) {
        return null;
      }
    
      React.useEffect(() => {
        // it's ok here, drawRef is not undefined
        console.log('useEffect drawRef when app is loading', drawRef);
      }, [drawRef]);
    
      const onCreateOrUpdate = React.useCallback(
        (e: { features: Feature[] }) => {
          // Here drawRef would not be undefined
          console.log('drawRef under onCreateOrUpdate method', drawRef);
        },
        [drawRef]
      );
    
      return (
        <DrawControl
          ref={drawRef}
          position="top-right"
          displayControlsDefault={false}
          controls={{
            polygon: true,
            trash: true,
          }}
          defaultMode="simple_select"
          modes={{ ...MapboxDraw.modes, draw_rectangle: DrawRectangle }}
          onCreate={onCreateOrUpdate}
          onUpdate={onCreateOrUpdate}
        />
      );
    };