2

Following the Mapbox draw example I can use the draw variable to access all features that are drawn on a map.

const draw = new MapboxDraw({
 // ...
});
map.addControl(draw);
// ...
function updateArea(e) {
  const data = draw.getAll(); // Accessing all features (data) drawn here
  // ...
}

However, in react-map-gl library useControl example I can not figure out how to pass ref to the DrawControl component so I can use it as something like draw.current in a similar way as I did draw in normal javascript above.

In my DrawControl.jsx

const DrawControl = (props) => {
  useControl(
    ({ map }) => {
      map.on('draw.create', props.onCreate);
      // ...
      return new MapboxDraw(props);
    },
    ({ map }) => {
      map.off('draw.create', props.onCreate);
      // ...
    },{
      position: props.position,
    },
  );

  return null;
};

In my MapDrawer.jsx

import Map from 'react-map-gl';
import DrawControl from './DrawControl';
// ...
export const MapDrawer = () => {
  const draw = React.useRef(null);

  const onUpdate = React.useCallback((e) => {
    const data = draw.current.getAll(); // this does not work as expected
    // ...
  }, []);

  return (
    // ...
    <Map ...>
      <DrawControl
        ref={draw}
        onCreate={onUpdate}
        onUpdate={onUpdate}
        ...
      />
    </Map>
  )
}

I also get an error stating I should use forwardRef but I'm not really sure how.

react_devtools_backend.js:3973 Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

What I need is basically to delete the previous feature if there is a new polygon drawn on a map so that only one polygon is allowed on a map. I want to be able to do something like this in the onUpdate callback.

const onUpdate = React.useCallback((e) => {
  // ...
  draw.current.delete(draw.current.getAll.features[0].id);
  // ...
}, []);
Drejc
  • 491
  • 6
  • 21

3 Answers3

3

I had the similar problem recently with that lib, I solved it doing the following :

export let drawRef = null; 
export default const DrawControl = (props) => {
  drawRef = useControl(
    ({ map }) => {
      map.on('draw.create', props.onCreate);
      // ...
      return new MapboxDraw(props);
    },
    ({ map }) => {
      map.off('draw.create', props.onCreate);
      // ...
    },{
      position: props.position,
    },
  );

  return null;
};
import DrawControl, {drawRef} from './DrawControl';
// ...
export const MapDrawer = () => {
  const draw = drawRef;

  const onUpdate = React.useCallback((e) => {
    const data = draw?draw.current.getAll():null; // this does not work as expected
    // ...
  }, []);

  return (
    // ...
    <Map ...>
      <DrawControl
        onCreate={onUpdate}
        onUpdate={onUpdate}
        ...
      />
    </Map>
  )
}
const onUpdate = React.useCallback((e) => {
  // ...
  drawRef.delete(drawRef.getAll.features[0].id);
  // ...
}, []);

Once component created, the ref is available for use. Not that elegant but working... Sure there might be cleaner way...

Hope that helps! Cheers

limaNz
  • 46
  • 2
2

I think I found a better solution combine forwardRef and useImperativeHandle to solve:

export const DrawControl = React.forwardRef((props: DrawControlProps, ref) => {
  const drawRef = 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,
    }
  );

  React.useImperativeHandle(ref, () => drawRef, [drawRef]); // This way I exposed drawRef outside the component

  return null;
});

in the component:

const drawRef = React.useRef<MapboxDraw>();

const [drawMode, setDrawMode] = React.useState<DrawMode>(“draw_polygon");

const changeModeTo = (mode: DrawMode) => {
  // If you programmatically invoke a function in the Draw API, any event that directly corresponds with that function will not be fired
  drawRef.current?.changeMode(mode as string);
  setDrawMode(mode);
};

<>
<DrawControl
  ref={drawRef}
  position="top-right”
  displayControlsDefault={false}
  controls={{
    polygon: true,
    trash: true,
  }}
  defaultMode=“draw_polygon"
  onCreate={onUpdate}
  onUpdate={onUpdate}
  onDelete={onDelete}
  onModeChange={onModeChange}
  />

<button
  style={{
  position: ‘absolute’,
  left: ‘20px’,
  top: ‘20px’,
  backgroundColor: '#ff0000’,
  }}
  onClick={() => changeModeTo('simple_select’)}
  >
  Change to Simple Select
</button>

<>
Joel
  • 974
  • 2
  • 10
  • 18
0

Pass draw from draw control to parent component.

    const DrawControl = (props) => {
        const [draw, setDraw] = useState()
        const { setDraw: setDrawInParent, onUpdate, onCreate, onDelete } = props;

        useEffect(() => {
            if (draw) setDrawInParent(draw)
        }, [draw])

        useControl(
            ({ map }) => {
                map.on("draw.create", onCreate);
                map.on("draw.update", onUpdate);
                map.on("draw.delete", onDelete);
                const draw = new MapboxDraw(props);
                setDraw(draw);
                return draw;
            }
        );

        return null;
    };
  • Why would you need the local draw state and the useEffect? Why not just call props.setDrawInParent(draw) directly after it's created? – Knut Marius Sep 22 '22 at 12:22