Search code examples
reactjsuse-effectpolylinereact-leaflet

React-Leaflet: Cannot create a Polyline DYNAMICALLY from React Context


UPDATE! As Seth Luke asked, why a ref instead of a state, so I did that, and now the lines get drawn! but one step behind. Check out these lines:

useEffect(()=>{
        if (drawing) {
                setZonePolygon((prev)=>[...prev, [clickLocation.lat, clickLocation.lng]]);
                setContextData((prevContext)=>({...prevContext, lines: zonePolygon}));
                addZoneMarker();
            }
        }, [clickLocation]);

"lines" in context is getting updated one step behind the local state "zonePolygon"... how do I correct this? Even if I switch the calls, it's the same, the Context gets updated with a delay... enter image description here

ORIGINAL POST:

I'm connected to a context in my main map component which contains a . I'm changing the context from another component expecting my map container component to update and re-render the polyline, but it is not happening. What am I doing wrong here? I'm really tired of reading and trying all sort of stuff for over 15 hours no rest now. Could anybody help please? I'd really appreciate it.

My goal is to let the user click different points in the map and have those joined with a line, so that then I can save that as an area or "zone".

This is not being called, I wonder why! I'm using react dev tools to debug and the context does indeed gets the changes, but it's not triggering in the component... so weird.

useEffect(()=>{
            console.log('Lines updated in Map component via Context.', lines);
        }, [lines]); // This is not being called, I wonder why!!! ****

This is the code I have:

import React, {useState, useEffect, useContext, useRef} from 'react';
import {MapContainer, Marker, Polyline, Polygon, useMapEvent} from 'react-leaflet';
import 'leaflet-rotatedmarker';
import {MapContext} from '../../context/MapProvider';
import Layers from './Layers';
import Ships from '../Ships';

const Map = () => {
    const [map, setMap] = useState(null);
    const {contextData, setContextData} = useContext(MapContext);
    const {clickLocation, drawing, lines} = contextData;
    const [shipData, setShipData] = useState();
        
    useEffect(()=>{
        console.log('Lines updated in Map component via Context.', lines);
    }, [lines]); // This is not being called, I wonder why!!! ****



    useEffect(()=>{
        if (!map) return;
        setContextData({...contextData, mapRef: map});
    }, [map]);

    useEffect(() => {
        setShipData(contextData.vessels);
    }, [contextData.vessels]);

    function MapEvents() {
        const map = useMapEvent('click', (e) => {
        setContextData({...contextData, clickLocation: e.latlng});
        });
        return null;
    }

    // const ZONE = [
    //  [-41.95208616893812, -73.52483926124243],
    //  [-42.246913395396184, -73.17047425039003],
    //  [-42.19905906325171, -72.68013196793146],
    //  [-41.936746304733255, -72.81473573174362],
    //  [-41.8118450173935, -73.22404105435608],
    // ]

    return (
        <MapContainer
                center={[-42, -73]} 
                zoom={10}   
                style={{height: '100%', width: '100%'}} 
                whenCreated={setMap}>
            <MapEvents />
            <Layers />      
            <Ships data={shipData} />
            {
                (drawing & lines.length > 1) ? <Polyline positions={lines} /> : null
            }
        </MapContainer>
    )
}

export default Map;

And this is where I'm modifying the context at:

import React, {useState, useEffect, useRef, useContext} from 'react';
import L from 'leaflet';
import styles from '../../styles.module.scss';
import ZoneItem from './ZoneItem';
import { MapContext } from './../../../../context/MapProvider';

const ZonesBar = () => {
    const {contextData, setContextData} = useContext(MapContext);
    const {mapRef, drawing, lines, clickLocation} = contextData;
    const [zones, setZones] = useState([]);
    const [zoneMarkers, setZoneMarkers] = useState([]);
    let zonePolygon = useRef([]);

    useEffect(()=>{
        if (drawing) {
            setContextData((contextData)=>({...contextData, lines: []}));
            zonePolygon.current = [];
        } else if (!drawing) {
            if (zonePolygon.current.length > 2) {
                setContextData((prevContext)=>({...prevContext, zones: [...prevContext.zones, contextData.lines]}));
                setZones((prevZones)=>([...prevZones, zonePolygon.current]));
                clearMarkers();
            }
        }
    }, [drawing]);

    useEffect(()=>{
        if (drawing) {
                zonePolygon.current.push([clickLocation.lat, clickLocation.lng]);
                setContextData((prevContext)=>({...prevContext, lines: zonePolygon.current}));
                addZoneMarker();
            }
        }, [clickLocation]);

    function toggleDrawing() {
        setContextData((prevContext)=>({...prevContext, drawing: !prevContext.drawing}))
    }

    function addZoneMarker() {
        const newMarker = L.marker([clickLocation.lat, clickLocation.lng])
            .addTo(mapRef);
        setZoneMarkers((prevMarkers)=>([...prevMarkers, newMarker]));
    }

    function clearMarkers() {
        zoneMarkers.forEach(m => mapRef.removeLayer(m));
    }

    return (
        <div className={styles.zones}>
            <button 
                className={`${styles.btn_add} ${drawing ? styles.btn_drawing : ''}`}
                onClick={toggleDrawing}
                >
                    {drawing ? 'Agregar zona' : 'Definir zona'}
            </button>
            <span style={{fontSize: '0.7rem', fontStyle: 'italic', marginLeft: '0.5rem',}}>
                {drawing ? 'Dar clicks en el mapa para definir la zona, luego presionar el botón otra vez.' : ''}
            </span>

            <div className={styles.list}>
            {
                    zones.length > 0 ?
                    zones.map(zone => <ZoneItem data={zone} />)
                    :
                    'Lista vacía.'
                }                       
            </div>
        </div>  
    )
}

export default ZonesBar;

I've changed things up so much now since 9 am today, that I don't know anything else anymore. There's obviously a way of doing this, and I do need some help. If you could take your time to go through this issue that'd be life saving for me. This is what is looks like, see when I render it with a hard-coded array the polyline comes up.

This is my Context:

import React, {useState, useEffect, createContext, useContext} from 'react'
import io from 'socket.io-client'
import axios from 'axios';

export const MapContext = createContext();
const socket = io("http://localhost:3001");

const MapProvider = ({children}) => {
    const [contextData, setContextData] = useState({
        mapRef: null,
        clickLocation: [],
        markers: [],
        zones: [],
        drawing: false,
        lines: [],
        vessels: []
    });
    
    // Bring vessels info from API and store in Context.
    useEffect(()=>{
        axios.get('http://localhost:3001/vessel/search/all')
        .then(res => {
            setContextData((prevContext)=>({...prevContext, vessels: res.data}));
        })
        .then(()=>{
            socket.on('vessels', data => {
            setContextData((prevContext)=>({...prevContext, vessels: data}));
            })
        })
        .catch(err => console.log(err.message));
    }, []);

    return (
        <MapContext.Provider value={{contextData, setContextData}}>
            {children}
        </MapContext.Provider>
    )
}

export default MapProvider;

enter image description here


Solution

  • It looks like what Seth Lutske suggested in the comments to the original question plus some adjustments did the trick. I wish he could post it as an answer so that I accept it as the solution.

    Basically the solution was to use a state hook:

    const [zonePolygon, setZonePolygon] = useState([]);
    

    Instead of a useRef:

    const zonePolygon = useRef();
    

    Then to have the local state and the global Context update in order I split them in different useEffects. This is the working code, but I believe it needs a refactor:

    useEffect(()=>{
            if (drawing) {
                setZonePolygon((prev)=>[...prev, [clickLocation.lat, clickLocation.lng]]);
                addZoneMarker();
            }
        }, [clickLocation]);
    
        useEffect(()=>{
            setContextData((prevContext)=>({...prevContext, lines: zonePolygon}));
        }, [zoneMarkers]);
    

    enter image description here