Search code examples
javascriptreactjsleafletreact-leafletreact-leaflet-v3

updating props when using createControlComponent in react-leaflet 3


Following the offical reference for Higher Level Component Factory to update props for a Control Component

The core APIs export other high-level component factories that can be used in a similar way.

I've mimicked the example - but I get a syntax error for the following:

import L from "leaflet";
import "leaflet-routing-machine";
import { createControlComponent } from "@react-leaflet/core";
import 'leaflet-routing-machine/dist/leaflet-routing-machine.css'

function setWaypoints(props)
{
    return { 
        waypoints: [
        L.latLng(props.startLat, props.startLng),
        L.latLng(props.endLat, props.endLng)
        ],
        lineOptions: {
            styles: [{ color: "#0500EE", weight: 4 }]
        },
        show: false,
        addWaypoints: false,
        routeWhileDragging: true,
        draggableWaypoints: true,
        fitSelectedRoutes: true,
        showAlternatives: false,
        createMarker: function() { return null; },

    }
}


function createRoutingMachine(props, context) 
{
    const instance =  new L.Routing.control(setWaypoints(props))
    return 
    { 
        instance, context: { ...context, overlayContainer: instance }    
    }
}


function updateRoutingMachine(instance, props, prevProps)
{
    if (props.endLat !== prevProps.endLat || props.endLng !== prevProps.endLng) 
    {
        instance.setWaypoints(props)
    }
}

const RoutingMachine = createControlComponent(createRoutingMachine, updateRoutingMachine)
export default RoutingMachine;

Missing semicolon. (35:25)

33 | return 34 | {

35 | instance, context: { ...context, overlayContainer: instance }
| ^ 36 | }

If I change this to:


function createRoutingMachine(props) 
{
    const instance =  new L.Routing.control(setWaypoints(props))
    return instance
}

The compiler is happy, but the component never updates.

I know I'm creating the Control Component incorrectly, but I can't find the information for the correct implementation.

Related:
How to use Leaflet Routing Machine with React-Leaflet 3?
How to extend TileLayer component in react-leaflet v3?


Solution

  • You will notice that in the docs, createcontrolcomponent lists only one argument, which is a function to create the instance. You are expecting it to behave like createlayercomponent, which takes two arguments. In createlayercomponent, the second argument is a function to update the layer component when the props change. However, createcontrolcomponent offers no such functionality. react-leaflet is assuming, much like vanilla leaflet, that once your control is added to the map, you won't need to alter it directly.

    This gets a bit confusing in terms of leaflet-routing-machine, because you don't need to change the instance of the control, but rather you need to call a method on it which affects the map presentation.

    IMO, the best way to go is to use a state variable to keep track of whether or not your waypoints have changed, and use a ref to access the underlying leaflet instance of the routing machine, and call setWayPoints on that:

    // RoutineMachine.jsx
    
    const createRoutineMachineLayer = (props) => {
      const { waypoints } = props;
      const instance = L.Routing.control({
        waypoints,
        ...otherOptions
      });
    
      return instance;
    };
    
    // Takes only 1 argument:
    const RoutingMachine = createControlComponent(createRoutineMachineLayer);
    
    // Map.jsx
    
    const Map = (props) => {
      // create a ref
      const rMachine = useRef();
      // create some state variable, any state variable, to track changes
      const [points, setPoints] = useState(true);
      const pointsToUse = points ? points1 : points2;
    
      // useEffect which responds to changes in waypoints state variable
      useEffect(() => {
        if (rMachine.current) {
          rMachine.current.setWaypoints(pointsToUse);
        }
      }, [pointsToUse, rMachine]);
    
      return (
        <MapContainer {...props}>
          <RoutineMachine ref={rMachine} waypoints={pointsToUse} />
          <button onClick={() => setPoints(!points)}>
            Toggle Points State and Props
          </button>
        </MapContainer>
      );
    };
    

    Working Codesandbox

    Bonus: a cheap and easy way to force a rerender on your <RoutineMachine> component (or any react component) is to assign it a key prop, and change that key prop when you want to rerender it. This might be a uuid, or even a unique set of waypoints ran through JSON.stringify. Just an idea.