Skip to content

Commit

Permalink
fix map unmounting in component lifecycle (#276)
Browse files Browse the repository at this point in the history
  • Loading branch information
danangmassandy authored Jan 24, 2025
1 parent fadd145 commit 01b269b
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 98 deletions.
9 changes: 6 additions & 3 deletions django_project/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import theme from "./theme";
import store from "./store";
import ProjectRoutes from "./Routes";
import { SessionProvider } from "./sessionProvider";
import { MapProvider } from './MapContext';

Sentry.init({
dsn: (window as any).sentryDsn,
Expand Down Expand Up @@ -42,9 +43,11 @@ function App() {
<ChakraProvider theme={theme}>
<Provider store={store}>
<SessionProvider>
<Router>
<ProjectRoutes />
</Router>
<MapProvider>
<Router>
<ProjectRoutes />
</Router>
</MapProvider>
</SessionProvider>
</Provider>
</ChakraProvider>
Expand Down
26 changes: 26 additions & 0 deletions django_project/frontend/src/MapContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React, { createContext, useContext, useState } from 'react';
import { Map } from 'maplibre-gl';

interface MapContextProps {
map: Map | null;
setMap: React.Dispatch<React.SetStateAction<Map | null>>;
}

const MapContext = createContext<MapContextProps | undefined>(undefined);

export const MapProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [map, setMap] = useState<Map | null>(null);
return (
<MapContext.Provider value={{ map, setMap }}>
{children}
</MapContext.Provider>
);
};

export const useMap = (): MapContextProps => {
const context = useContext(MapContext);
if (!context) {
throw new Error('useMap must be used within a MapProvider');
}
return context;
};
24 changes: 3 additions & 21 deletions django_project/frontend/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,27 +87,9 @@ export default function Header(props: any) {
zIndex={10}
>
<ListItem>
{/*
# TODO: This is currently a hacky solution.
If we don't refresh the map, it causes multiple render issues.
We need to investigate the root cause.
Currently, forcing a page reload ensures the map resets.
*/}
<Text
as="a"
href="/#/map"
onClick={(e) => {
e.preventDefault();
window.location.href = "/#/map";
window.location.reload();
}}
_hover={{ textDecoration: "underline", textDecorationColor: "white" }}
color="white"
fontWeight={400}
fontSize={16}
>
MAP
</Text>
<Link href="/#/map" _hover={{ textDecoration: 'underline', textDecorationColor: 'white' }}>
<Text color="white" fontWeight={400} fontSize={16}>MAP</Text>
</Link>
</ListItem>
<ListItem>
<Link
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import React, { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react';
import { useSelector } from "react-redux";
import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react';
import {FeatureCollection} from "geojson";
import { combine } from "@turf/combine";
import { area } from "@turf/area";
import MapboxDraw from "@mapbox/mapbox-gl-draw";
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
import { RootState } from "../../../../store";
import {CUSTOM_GEOM_ID} from "../../DataTypes";
import { removeSource } from '../../utils';
import { useMap } from '../../../../MapContext';

export const CUSTOM_GEOM_FILL_ID = CUSTOM_GEOM_ID + "-fill";

Expand Down Expand Up @@ -96,8 +95,7 @@ const styles = [
export const AnalysisCustomGeometrySelector = forwardRef((
{ isDrawing, onSelected }: Props, ref
) => {
const [map, setMap] = useState<maplibregl.Map>(null);
const { mapInitiated } = useSelector((state: RootState) => state.mapConfig);
const { map } = useMap();
const drawingRef = useRef(null);

useImperativeHandle(ref, () => ({
Expand All @@ -119,16 +117,10 @@ export const AnalysisCustomGeometrySelector = forwardRef((
}

useEffect(() => {
try {
window.map.getStyle()
const _map = window.map
setMap(_map)
} catch (err) {
console.log(err)
if (!map) {
return;
}
}, [window.map, mapInitiated])

useEffect(() => {
if (isDrawing) {
removeSource(map, CUSTOM_GEOM_ID);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import maplibregl from "maplibre-gl";
import { COMMUNITY_ID } from "../../DataTypes";
import { Community, Landscape } from "../../../../store/landscapeSlice";
import { useSelector } from "react-redux";
import { RootState } from "../../../../store";
import { useMap } from '../../../../MapContext';

const COMMUNITY_FILL_ID = COMMUNITY_ID + '-fill';

Expand All @@ -24,25 +23,23 @@ let clickFunction: (ev: maplibregl.MapMouseEvent & {
export default function AnalysisLandscapeGeometrySelector(
{ landscape, enableSelection, onSelected }: Props
) {
const [map, setMap] = useState<maplibregl.Map>(null);
const { mapInitiated } = useSelector((state: RootState) => state.mapConfig);
const { map } = useMap();

useEffect(() => {
try {
window.map.getStyle()
const _map = window.map
setMap(_map)

if (!map) {
return
}
// render community layer
_map.addSource(
map.addSource(
COMMUNITY_ID, {
type: 'vector',
tiles: [
document.location.origin + '/frontend-api/landscapes/vector_tile/{z}/{x}/{y}/'
]
}
);
_map.addLayer({
map.addLayer({
'id': COMMUNITY_ID,
'type': 'line',
'source': COMMUNITY_ID,
Expand All @@ -52,7 +49,7 @@ export default function AnalysisLandscapeGeometrySelector(
'line-width': 1
}
});
_map.addLayer({
map.addLayer({
'id': COMMUNITY_ID + '-community',
'type': 'line',
'source': COMMUNITY_ID,
Expand All @@ -63,7 +60,7 @@ export default function AnalysisLandscapeGeometrySelector(
},
"filter": ["==", "landscape_id", 0]
});
_map.addLayer({
map.addLayer({
'id': COMMUNITY_ID + '-highlight',
'type': 'fill',
'source': COMMUNITY_ID,
Expand All @@ -74,7 +71,7 @@ export default function AnalysisLandscapeGeometrySelector(
},
"filter": ["==", "landscape_id", 0]
});
_map.addLayer({
map.addLayer({
'id': COMMUNITY_FILL_ID,
'type': 'fill',
'source': COMMUNITY_ID,
Expand All @@ -87,7 +84,7 @@ export default function AnalysisLandscapeGeometrySelector(
} catch (err) {
console.log(err)
}
}, [window.map, mapInitiated])
}, [map])

useEffect(() => {
if (!map) {
Expand Down
101 changes: 62 additions & 39 deletions django_project/frontend/src/components/Map/MapLibre.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@ import React, {
useRef,
useState
} from 'react';
import maplibregl from 'maplibre-gl';
import maplibregl, { FillLayerSpecification } from 'maplibre-gl';
import { Box } from "@chakra-ui/react";
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "../../store";
import { BasemapControl, LegendControl } from "./control";
import { hasSource, removeLayer, removeSource } from "./utils";
import { fetchBaseMaps } from '../../store/baseMapSlice';
import { fetchMapConfig, mapInitated } from '../../store/mapConfigSlice';
import { fetchMapConfig } from '../../store/mapConfigSlice';
import { Layer, setSelectedNrtLayer } from '../../store/layerSlice';
import { selectIsLoggedIn } from "../../store/authSlice";
import { COMMUNITY_ID } from "./DataTypes";
import { useMap } from '../../MapContext';

import 'maplibre-gl/dist/maplibre-gl.css';
import './style.css';
Expand All @@ -33,7 +34,7 @@ export const MapLibre = forwardRef(
const dispatch = useDispatch<AppDispatch>();
const legendRef = useRef(null);
const baseMapRef = useRef(null);
const [map, setMap] = useState(null);
const { map, setMap } = useMap();
const { mapConfig } = useSelector((state: RootState) => state.mapConfig);
const { baseMaps } = useSelector((state: RootState) => state.baseMap);
const { selected: selectedLandscape } = useSelector((state: RootState) => state.landscape);
Expand Down Expand Up @@ -62,7 +63,7 @@ export const MapLibre = forwardRef(
)
}
}
let layerStyle = {
let layerStyle: FillLayerSpecification = {
"source": ID,
"id": ID,
"type": "fill",
Expand Down Expand Up @@ -138,44 +139,67 @@ export const MapLibre = forwardRef(

/** First initiate */
useEffect(() => {
if (baseMaps.length == 0 || !mapConfig) {
if (map) {
return;
}

const _map = new maplibregl.Map({
container: 'map',
style: {
version: 8,
sources: {},
layers: [],
glyphs: "/static/fonts/{fontstack}/{range}.pbf"
},
center: [0, 0],
zoom: 1
});

_map.once("load", () => {
setMap(_map)
})
_map.addControl(new BasemapControl(baseMaps, baseMapRef), 'bottom-left');
_map.addControl(new LegendControl(legendRef), 'top-left');
_map.addControl(new maplibregl.NavigationControl(), 'bottom-left');

// Clean up the map on component unmount
return () => {
// avoid removing map synchronously
setTimeout(() => {
_map.remove();
setMap(null)
}, 0)
};
}, [setMap]);

useEffect(() => {
if (!map) {
const _map = new maplibregl.Map({
container: 'map',
style: {
version: 8,
sources: {},
layers: [],
glyphs: "/static/fonts/{fontstack}/{range}.pbf"
},
center: [0, 0],
zoom: 1
});

// Save as global variable
window.map = _map;

_map.once("load", () => {
setMap(_map)

_map.fitBounds(mapConfig.initial_bound,
{
pitch: 0,
bearing: 0
}
)
return;
}
if (!mapConfig) {
return;
}

// render default base map
baseMapRef?.current?.setBaseMapLayer(baseMaps[0])
dispatch(mapInitated());
})
_map.addControl(new BasemapControl(baseMaps, baseMapRef), 'bottom-left');
_map.addControl(new LegendControl(legendRef), 'top-left');
_map.addControl(new maplibregl.NavigationControl(), 'bottom-left');
map.fitBounds(mapConfig.initial_bound,
{
pitch: 0,
bearing: 0
}
)
}, [map, mapConfig])

useEffect(() => {
if (!map) {
return;
}
if (baseMaps.length == 0) {
return;
}
}, [baseMaps, mapConfig]);

// render default base map
baseMapRef?.current?.setBaseMapLayer(baseMaps[0])
}, [map, baseMaps])


// zoom when landscape is selected
useEffect(() => {
Expand All @@ -197,7 +221,7 @@ export const MapLibre = forwardRef(
removeSource(map, ID)
}

map.fitBounds(selectedLandscape.bbox,
map.fitBounds(new maplibregl.LngLatBounds(selectedLandscape.bbox as [number, number, number, number]),
{
pitch: 0,
bearing: 0
Expand All @@ -217,4 +241,3 @@ export const MapLibre = forwardRef(
)
}
)

9 changes: 2 additions & 7 deletions django_project/frontend/src/store/mapConfigSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,10 @@ interface MapConfig {

interface MapConfigState extends DataState {
mapConfig: MapConfig;
mapInitiated: boolean;
}

const initialMapConfigState: MapConfigState = {
mapConfig: null,
mapInitiated:false,
loading: false,
error: null,
};
Expand All @@ -35,10 +33,7 @@ const mapConfigSlice = createSlice({
reducers: {
clearError(state) {
state.error = null;
},
mapInitated(state) {
state.mapInitiated = true;
},
}
},
extraReducers: (builder) => {
builder
Expand All @@ -56,6 +51,6 @@ const mapConfigSlice = createSlice({
}
});

export const { clearError, mapInitated } = mapConfigSlice.actions;
export const { clearError } = mapConfigSlice.actions;

export default mapConfigSlice.reducer;

0 comments on commit 01b269b

Please sign in to comment.