Search code examples
reactjsmaterial-uitabsdialogreact-leaflet

Why after switching views, React-Leaflet map grayed out? It happens after interaction with the map and then switching between views


I was making a dialog which has two tabs named Address and Map, which allows the user to enter city name and street name in two different textfields in address tab and be able to pinpoint or auto-locate the location in the map. In map tab I was using react-leaflet map to show the map itself to the user, so far so good but after switching between tabs the map changes to a monotonic gray image. zoom in and out won't help it!

Code:
import * as React from 'react';
import { useEffect, useState } from 'react';

import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import useMediaQuery from '@mui/material/useMediaQuery';
import CloseIcon from '@mui/icons-material/CloseOutlined';
import {   Divider, IconButton, InputLabel, } from '@mui/material';
import { Box, Grid, Tab, TextField, useTheme } from '@material-ui/core';
import { TabContext, TabList, TabPanel } from '@mui/lab';
import "leaflet/dist/leaflet.css";
import icon from "../../../../Account/components/constants";
import { MapContainer, TileLayer, Marker, useMapEvents, } from 'react-leaflet'

const useGeoLocation = () => {
    // this function will allow the user to get the current location of the device!
    const [location, setLocation] = useState({
        loaded: false,
        coordinates: { lat: "", lng: "" }
    });
    const onSuccess = (location) => {
        setLocation({
            loaded: true,
            coordinates: {
                lat: location.coords.latitude,
                lng: location.coords.longitude,
            }
        });
    };
    const onError = (error) => {
        setLocation({
            loaded: true,
            error,
        });
    };
    useEffect(() => {
        if (!("geolocation" in navigator)) {
            onError({
                code: 0,
                message: "Your device GPS is OFF!",
            });
        }
        navigator.geolocation.getCurrentPosition(onSuccess, onError);

    }, []);

    return location;
}
export default function AddressDialog() {
    // Genral Properties!
    const [open, setOpen] = useState(false);
    const theme = useTheme();
    const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
    const [value, setValue] = useState(0);

    // Address Tab Properties!
    const [city, setCity] = useState("");
    const [street, setStreet] = useState();
    
    // Map Properties!
    const [initializerPos,] = useState([40.689247, -74.044502]);
    const [position, setPosition] = useState(initializerPos);
    const [mapState, setMapState] = useState({
        position: position,
        map: null
    });
    const zoom_level = 18;
    const location = useGeoLocation();

    // Arrow funcitons!
    const handleClickOpen = () => {
        setOpen(true);
    };

    const handleClose = () => {
        setOpen(false);
        setValue(0);
    };

    const handleSubmit = () => {
        // api update in here
    }

    const showMyLocation = () => {
        if (location.loaded && !location.error) {
            let pos = [location.coordinates.lat, location.coordinates.lng];
            setPosition([location.coordinates.lat, location.coordinates.lng]);
            setMapState((state) => ({
                ...state,
                position: pos,
            }));

            const { map } = mapState;
            if (map) {
                map.flyTo(pos, zoom_level);
            }
        } else {
            if (location.error) {
                alert(location.error.message)
            }
            else {
                alert("Problem in loading curent location!")
            }
        }
    };
    function AddMarkerToClick() {

        const [markers, setMarkers] = useState([]);
        useMapEvents({
            click(e) {
                const newMarker = e.latlng
                setMarkers([...markers, newMarker]);
                setPosition([e.latlng.lat, e.latlng.lng]);
                setMapState((state) => ({
                    ...state,
                    position: newMarker,
                }));
                const { map } = mapState;
                if (map)
                {
                    map.flyTo(newMarker, zoom_level);
                }
            },
        });

        return null
    };
    
    return (
        <div dir="ltr">
            <Button onClick={handleClickOpen} variant="contained" type="button" aria-label="Edit Info" fullWidth size="small">
                Edit Address Info
            </Button>
            <Dialog fullScreen={fullScreen} open={open} aria-labelledby="responsive-dialog-title">
                <DialogTitle>
                    <IconButton onClick={handleClose} aria-label="Close Dialog">
                        <CloseIcon fontSize="medium" />
                    </IconButton>
                </DialogTitle>
                <Divider />

                <DialogTitle>Edit Address</DialogTitle>
                <DialogContent id ="dialogContent" >
                    <DialogContentText>
                        In this section you are able to edit your address info
                    </DialogContentText>
                    <TabContext value={value.toString()} >
                        <Box >
                            <TabList
                                onChange={(event, newValue) => { setValue(parseInt(newValue, 10));}}
                                aria-label="address-map-tab">
                                <Tab label="Address" value="0" />
                                <Tab label="Map" value="1" />
                            </TabList>
                        </Box>
                        
                        <TabPanel value="0">
                            <Grid container spacing={theme.spacing(0)}

                            >
                                <Grid item xs={12} sm={6}>
                                    <TextField value={city} onChange={(e) => setCity(e.target.value)} margin="normal" variant="outlined"
                                        required
                                        fullWidth
                                        type="text"
                                        name="area"
                                        id="area"
                                        label={"city"}
                                        placeholder={"ex: New York"}

                                    />
                                </Grid>
                                <Grid item xs={12} sm={6}>

                                    <TextField
                                        value={street}
                                        onChange={(e) => setStreet(e.target.value)}
                                        margin="normal"
                                        variant="outlined"
                                        required
                                        fullWidth
                                        type="text"
                                        name="street"
                                        id="street"
                                        label={"Streen Name"}
                                        placeholder={"ex: wall street"}

                                    />
                                </Grid>
                               
                            </Grid>
                        </TabPanel>
                        <TabPanel value="1">

                            <Grid container>

                                <div style={{
                                    marginLeft: "auto", 
                                    marginRight: "auto",
                                    width: "100%"
                                }}>
                                    <InputLabel>
                                        Your location in map: 
                                    </InputLabel>
                                        <MapContainer
                                            center={mapState.position}
                                            zoom ={15}
                                            scrollWheelZoom
                                            style={{
                                                height: fullScreen ? 200 : 350,
                                                width: fullScreen ? "100%" : "100%",
                                                textAlign: "center",
                                                marginLeft: "auto",
                                                marginRight: "auto",
                                                marginTop: theme.spacing(1)
                                            }}
                                            whenCreated={map => setMapState({ map })}
                                        >
                                            <TileLayer
                                                url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
                                            />
                                            <AddMarkerToClick />
                                            {
                                                position && (
                                                    <Marker position={position} icon={icon}></Marker>
                                                )
                                            }
                                        </MapContainer>
                                        <Button variant="outlined" color="primary" size="large" onClick={showMyLocation}>
                                            Here!
                                        </Button>
                                </div>
                            </Grid>
                        </TabPanel>
                    </TabContext>
                </DialogContent>
                <DialogActions style={{ marginLeft: theme.spacing(2), marginRight: theme.spacing(2) }}>
                    <Grid container direction="row" spacing={1}>
                        <Grid item container xs={4} dir="left" justifyContent="flex-end">
                            <Button variant="contained" type="button" color="error" fullWidth
                                name="cancel-btn" onClick={handleClose}
                            >
                                Cancel
                            </Button>
                        </Grid>
                        <Grid item container xs={8} >
                            <Button variant="contained" type="button" color="primary" fullWidth
                                name="submit-btn" onClick={handleSubmit} >
                                Save
                            </Button>
                        </Grid>
                    </Grid>
                </DialogActions>
            </Dialog>
        </div>
    );

After switching between tabs, the map grayed out, and shows nothing!

enter image description here

Then interacting with map, e.g, clicking on it, zoom in or out results the error!
Uncaught Error: Set map center and zoom first.

enter image description here


Solution

  • My Solution:

    What I did is to save the current (after first interaction with map) state of the map and then try to restart it from saved state for future use (switching to other tabs/views). To save the state of the map, It should happen right exactly before switching out from the map view! In this case before switching back to address tab or closing The Dialog.

    Fixed Code:
    import * as React from 'react';
    import { useEffect, useState } from 'react';
    
    import Button from '@mui/material/Button';
    import Dialog from '@mui/material/Dialog';
    import DialogActions from '@mui/material/DialogActions';
    import DialogContent from '@mui/material/DialogContent';
    import DialogContentText from '@mui/material/DialogContentText';
    import DialogTitle from '@mui/material/DialogTitle';
    import useMediaQuery from '@mui/material/useMediaQuery';
    import CloseIcon from '@mui/icons-material/CloseOutlined';
    import {   Divider, IconButton, InputLabel, } from '@mui/material';
    import { Box, Grid, Tab, TextField, useTheme } from '@material-ui/core';
    import { TabContext, TabList, TabPanel } from '@mui/lab';
    import "leaflet/dist/leaflet.css";
    import icon from "../../../../Account/components/constants";
    import { MapContainer, TileLayer, Marker, useMapEvents, } from 'react-leaflet'
    
    const useGeoLocation = () => {
        // this function will allow the user to get the current location of the device!
        const [location, setLocation] = useState({
            loaded: false,
            coordinates: { lat: "", lng: "" }
        });
        const onSuccess = (location) => {
            setLocation({
                loaded: true,
                coordinates: {
                    lat: location.coords.latitude,
                    lng: location.coords.longitude,
                }
            });
        };
        const onError = (error) => {
            setLocation({
                loaded: true,
                error,
            });
        };
        useEffect(() => {
            if (!("geolocation" in navigator)) {
                onError({
                    code: 0,
                    message: "Your device GPS is OFF!",
                });
            }
            navigator.geolocation.getCurrentPosition(onSuccess, onError);
    
        }, []);
    
        return location;
    }
    
    
    export default function AddressDialog() {
        // Genral Properties!
        const [open, setOpen] = useState(false);
        const theme = useTheme();
        const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
        const [value, setValue] = useState(0);
    
        // Address Tab Properties!
        const [city, setCity] = useState("");
        const [street, setStreet] = useState();
        
        // Map Properties!
        const [initializerPos,] = useState([40.689247, -74.044502]);
        const [position, setPosition] = useState(initializerPos);
        const [mapState, setMapState] = useState({
            position: position,
            map: null
        });
        const zoom_level = 18;
        const location = useGeoLocation();
    
        // Two new properties to overcome the gray out problem.
        const initializeZoom_level = 15;
        const [mapZoom, setMapZoom] = useState(initializeZoom_level);
    
        // Arrow funcitons!
        const stabilizeMapData = (isItADialogClose ) => {
            setMapZoom(isItADialogClose ? initializeZoom_level : mapZoom);
            setPosition(isItADialogClose ? initializerPos : position);
            setMapState({
                position: isItADialogClose ? initializerPos : position,
                map: null
            });
        }
    
        const handleClickOpen = () => {
            setOpen(true);
        };
    
        const handleClose = () => {
            stabilizeMapData(true); // The user is about to close The Dialog!
            setOpen(false);
            setValue(0);
        };
    
        const handleSubmit = () => {
            // api update in here
        }
    
        const showMyLocation = () => {
            if (location.loaded && !location.error) {
                let pos = [location.coordinates.lat, location.coordinates.lng];
                setPosition([location.coordinates.lat, location.coordinates.lng]);
                setMapState((state) => ({
                    ...state,
                    position: pos,
                }));
    
                const { map } = mapState;
                if (map) {
                    map.flyTo(pos, zoom_level);
                    setMapZoom(zoom_level);
                }
            } else {
                if (location.error) {
                    alert(location.error.message)
                }
                else {
                    alert("Problem in loading curent location!")
                }
            }
        };
    
        function AddMarkerToClick() {
    
            const [markers, setMarkers] = useState([]);
            useMapEvents({
                click(e) {
                    const newMarker = e.latlng
                    setMarkers([...markers, newMarker]);
                    setPosition([e.latlng.lat, e.latlng.lng]);
                    setMapState((state) => ({
                        ...state,
                        position: newMarker,
                    }));
                    const { map } = mapState;
                    if (map)
                    {
                        map.flyTo(newMarker, zoom_level);
                        setMapZoom(zoom_level);
                    }
                },
            });
    
            return null
        };
        
        return (
            <div dir="ltr">
                <Button onClick={handleClickOpen} variant="contained" type="button" aria-label="Edit Info" fullWidth size="small">
                    Edit Address Info
                </Button>
                <Dialog fullScreen={fullScreen} open={open} aria-labelledby="responsive-dialog-title">
                    <DialogTitle>
                        <IconButton onClick={handleClose} aria-label="Close Dialog">
                            <CloseIcon fontSize="medium" />
                        </IconButton>
                    </DialogTitle>
                    <Divider />
    
                    <DialogTitle>Edit Address</DialogTitle>
                    <DialogContent id ="dialogContent" >
                        <DialogContentText>
                            In this section you are able to edit your address info
                        </DialogContentText>
                        <TabContext value={value.toString()} >
                            <Box >
                                <TabList
                                    onChange={(event, newValue) => {
                                        if (value === 1) {
                                            // if it is in The Map Tab and It's about to switch the tab!
                                            stabilizeMapData(false);
                                        }
                                        
                                        setValue(parseInt(newValue, 10));
                                }}
                                    aria-label="address-map-tab">
                                    <Tab label="Address" value="0" />
                                    <Tab label="Map" value="1" />
                                </TabList>
                            </Box>
                            
                            <TabPanel value="0">
                                <Grid container spacing={theme.spacing(0)}
    
                                >
                                    <Grid item xs={12} sm={6}>
                                        <TextField value={city} onChange={(e) => setCity(e.target.value)} margin="normal" variant="outlined"
                                            required
                                            fullWidth
                                            type="text"
                                            name="area"
                                            id="area"
                                            label={"city"}
                                            placeholder={"ex: New York"}
    
                                        />
                                    </Grid>
                                    <Grid item xs={12} sm={6}>
    
                                        <TextField
                                            value={street}
                                            onChange={(e) => setStreet(e.target.value)}
                                            margin="normal"
                                            variant="outlined"
                                            required
                                            fullWidth
                                            type="text"
                                            name="street"
                                            id="street"
                                            label={"Streen Name"}
                                            placeholder={"ex: wall street"}
    
                                        />
                                    </Grid>
                                   
                                </Grid>
                            </TabPanel>
                            <TabPanel value="1">
    
                                <Grid container>
    
                                    <div style={{
                                        marginLeft: "auto", 
                                        marginRight: "auto",
                                        width: "100%"
                                    }}>
                                        <InputLabel>
                                            Your location in map: 
                                        </InputLabel>
                                            <MapContainer
                                                center={mapState.position}
                                                zoom={mapZoom}
                                                scrollWheelZoom
                                                style={{
                                                    height: fullScreen ? 200 : 350,
                                                    width: fullScreen ? "100%" : "100%",
                                                    textAlign: "center",
                                                    marginLeft: "auto",
                                                    marginRight: "auto",
                                                    marginTop: theme.spacing(1)
                                                }}
                                                whenCreated={map => setMapState({ map })}
                                            >
                                                <TileLayer
                                                    url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
                                                />
                                                <AddMarkerToClick />
                                                {
                                                    position && (
                                                        <Marker position={position} icon={icon}></Marker>
                                                    )
                                                }
                                            </MapContainer>
                                            <Button variant="outlined" color="primary" size="large" onClick={showMyLocation}>
                                                Here!
                                            </Button>
                                    </div>
                                </Grid>
                            </TabPanel>
                        </TabContext>
                    </DialogContent>
                    <DialogActions style={{ marginLeft: theme.spacing(2), marginRight: theme.spacing(2) }}>
                        <Grid container direction="row" spacing={1}>
                            <Grid item container xs={4} dir="left" justifyContent="flex-end">
                                <Button variant="contained" type="button" color="error" fullWidth
                                    name="cancel-btn" onClick={handleClose}
                                >
                                    Cancel
                                </Button>
                            </Grid>
                            <Grid item container xs={8} >
                                <Button variant="contained" type="button" color="primary" fullWidth
                                    name="submit-btn" onClick={handleSubmit} >
                                    Save
                                </Button>
                            </Grid>
                        </Grid>
                    </DialogActions>
                </Dialog>
            </div>
        );
    }
    
    

    I did some comments in the code to demonstrate the solution. If you know a better solution to this problem I looking forward to see it.