Skip to content

Commit

Permalink
refactor(frontend): concurrent rendering in React 18
Browse files Browse the repository at this point in the history
- use the new `createRoot` API in React 18.
- refactor the map layers to avoid an error in React Map GL when the map first mounts.
  • Loading branch information
eatyourgreens committed Jun 25, 2024
1 parent 2850a4f commit 16a42a0
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { getAssetDataFormats } from 'config/assets/data-formats';
import { FeatureSidebarContent } from 'details/features/FeatureSidebarContent';
import { BoundingBox, extendBbox } from 'lib/bounding-box';
import { colorMap } from 'lib/color-map';
import { mapFitBoundsState } from 'map/MapView';
import { mapFitBoundsState } from 'lib/map/MapBoundsFitter';
import { ColorBox } from 'map/tooltip/content/ColorBox';
import { useCallback, useMemo } from 'react';
import { atom, useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
Expand Down
23 changes: 17 additions & 6 deletions frontend/src/lib/data-map/BaseMap.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
import { MapViewState } from 'deck.gl/typed';
import { ComponentProps, FC, ReactNode } from 'react';
import { FC, ReactNode } from 'react';
import { Map } from 'react-map-gl/maplibre';
import { useRecoilState, useRecoilValue } from 'recoil';

import { backgroundState, showLabelsState } from 'map/layers/layers-state';
import { useBasemapStyle } from 'map/use-basemap-style';
import { mapViewStateState } from 'state/map-view/map-view-state';

export interface BaseMapProps {
mapStyle: ComponentProps<typeof Map>['mapStyle'];
viewState: MapViewState;
onViewState: (vs: MapViewState) => void;
children?: ReactNode;
}

/**
* Displays a react-map-gl basemap component.
* Accepts children such as a DeckGLOverlay, HUD controls etc
*/
export const BaseMap: FC<BaseMapProps> = ({ mapStyle, viewState, onViewState, children }) => {
export const BaseMap: FC<BaseMapProps> = ({ children }) => {
const background = useRecoilValue(backgroundState);
const showLabels = useRecoilValue(showLabelsState);
const [viewState, setViewState] = useRecoilState(mapViewStateState);
const { mapStyle } = useBasemapStyle(background, showLabels);

function handleViewStateChange({ viewState }: { viewState: MapViewState }) {
setViewState(viewState);
}

return (
<Map
reuseMaps={true}
styleDiffing={true}
{...viewState}
onMove={({ viewState }) => onViewState(viewState)}
onMove={handleViewStateChange}
mapStyle={mapStyle}
dragRotate={false}
keyboard={false}
Expand Down
40 changes: 24 additions & 16 deletions frontend/src/lib/data-map/DataMap.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import type { MapboxOverlay } from '@deck.gl/mapbox/typed';
import { useMap } from 'react-map-gl/maplibre';
import { FC, useCallback, useMemo, useRef } from 'react';
import { FC, useCallback, useEffect, useMemo, useRef } from 'react';
import { useRecoilValue } from 'recoil';

import { interactionGroupsState } from 'state/layers/interaction-groups';
import { viewLayersFlatState } from 'state/layers/view-layers-flat';
import { useSaveViewLayers, viewLayersParamsState } from 'state/layers/view-layers-params';
import { backgroundState, showLabelsState } from 'map/layers/layers-state';
import { useBasemapStyle } from 'map/use-basemap-style';

import { useTriggerMemo } from '../hooks/use-trigger-memo';
import { useDataLoadTrigger } from './use-data-load-trigger';
Expand All @@ -10,27 +17,28 @@ import { useInteractions } from './interactions/use-interactions';
import { ViewLayer, ViewLayerParams } from './view-layers';
import { LayersList } from 'deck.gl/typed';

export interface DataMapProps {
beforeId: string;
viewLayers: ViewLayer[];
viewLayersParams: Record<string, ViewLayerParams>;
interactionGroups: any;
}

// set a convention where the view layer id is either the first part of the deck id before the @ sign, or it's the whole id
function lookupViewForDeck(deckLayerId: string) {
return deckLayerId.split('@')[0];
}

export const DataMap: FC<DataMapProps> = ({
beforeId,
viewLayers,
viewLayersParams,
interactionGroups,
}) => {
export const DataMap: FC = () => {
const deckRef = useRef<MapboxOverlay>();
const { current: map } = useMap();
const zoom = map.getMap().getZoom();
const background = useRecoilValue(backgroundState);
const showLabels = useRecoilValue(showLabelsState);
const viewLayers = useRecoilValue(viewLayersFlatState);
const saveViewLayers = useSaveViewLayers();
const { firstLabelId } = useBasemapStyle(background, showLabels);

useEffect(() => {
saveViewLayers(viewLayers);
}, [saveViewLayers, viewLayers]);

const viewLayersParams = useRecoilValue(viewLayersParamsState);

const interactionGroups = useRecoilValue(interactionGroupsState);

const dataLoaders = useMemo(
() =>
Expand All @@ -45,9 +53,9 @@ export const DataMap: FC<DataMapProps> = ({
const layersFunction = useCallback(
({ zoom }: { zoom: number }) =>
viewLayers.map((viewLayer) =>
makeDeckLayers(viewLayer, viewLayersParams[viewLayer.id], zoom, beforeId),
makeDeckLayers(viewLayer, viewLayersParams[viewLayer.id], zoom, firstLabelId),
) as LayersList,
[beforeId, viewLayers, viewLayersParams],
[firstLabelId, viewLayers, viewLayersParams],
);

const { onHover, onClick, layerFilter, pickingRadius } = useInteractions(
Expand Down
17 changes: 13 additions & 4 deletions frontend/src/lib/map/MapBoundsFitter.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import { WebMercatorViewport } from 'deck.gl/typed';
import { FC, useEffect } from 'react';
import { useMap } from 'react-map-gl/maplibre';
import { atom, useRecoilValue, useResetRecoilState } from 'recoil';

import { BoundingBox, appToDeckBoundingBox } from '../bounding-box';

interface MapBoundsFitterProps {
boundingBox: BoundingBox;
}
export const mapFitBoundsState = atom<BoundingBox>({
key: 'mapFitBoundsState',
default: null,
});

export const MapBoundsFitter: FC<MapBoundsFitterProps> = ({ boundingBox }) => {
export const MapBoundsFitter: FC = () => {
const { current: map } = useMap();
const boundingBox = useRecoilValue(mapFitBoundsState);

const resetFitBounds = useResetRecoilState(mapFitBoundsState);
useEffect(() => {
// reset map fit bounds whenever map is mounted
resetFitBounds();
}, [resetFitBounds]);

useEffect(() => {
if (boundingBox != null && map != null) {
Expand Down
62 changes: 9 additions & 53 deletions frontend/src/map/MapView.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import { Suspense, useCallback, useEffect } from 'react';
import {
atom,
useRecoilState,
useRecoilValue,
useResetRecoilState,
useSetRecoilState,
} from 'recoil';
import { useSetRecoilState } from 'recoil';

import { mapViewStateState } from '../state/map-view/map-view-state';
import { BoundingBox } from 'lib/bounding-box';
import { BaseMap } from 'lib/data-map/BaseMap';
import { DataMap } from 'lib/data-map/DataMap';
import { DataMapTooltip } from 'lib/data-map/DataMapTooltip';
import { MapBoundsFitter } from 'lib/map/MapBoundsFitter';
import { MapBoundsFitter, mapFitBoundsState } from 'lib/map/MapBoundsFitter';
import { MapHud } from 'lib/map/hud/MapHud';
import { MapHudRegion } from 'lib/map/hud/MapHudRegion';
import {
Expand All @@ -25,22 +17,12 @@ import { PlaceSearchResult } from 'lib/map/place-search/use-place-search';
import { ErrorBoundary } from 'lib/react/ErrorBoundary';
import { withProps } from 'lib/react/with-props';

import { interactionGroupsState } from 'state/layers/interaction-groups';
import { viewLayersFlatState } from 'state/layers/view-layers-flat';
import { useSaveViewLayers, viewLayersParamsState } from 'state/layers/view-layers-params';
import { globalStyleVariables } from '../theme';
import { useIsMobile } from '../use-is-mobile';

import { MapLayerSelection } from './layers/MapLayerSelection';
import { backgroundState, showLabelsState } from './layers/layers-state';
import { MapLegend } from './legend/MapLegend';
import { TooltipContent } from './tooltip/TooltipContent';
import { useBasemapStyle } from './use-basemap-style';

export const mapFitBoundsState = atom<BoundingBox>({
key: 'mapFitBoundsState',
default: null,
});

const AppPlaceSearch = () => {
const setFitBounds = useSetRecoilState(mapFitBoundsState);
Expand Down Expand Up @@ -114,52 +96,26 @@ const MapHudMobileLayout = () => {
};

const MapViewContent = () => {
const [viewState, setViewState] = useRecoilState(mapViewStateState);
const background = useRecoilValue(backgroundState);
const showLabels = useRecoilValue(showLabelsState);
const viewLayers = useRecoilValue(viewLayersFlatState);
const saveViewLayers = useSaveViewLayers();
const { mapStyle, firstLabelId } = useBasemapStyle(background, showLabels);

useEffect(() => {
saveViewLayers(viewLayers);
}, [saveViewLayers, viewLayers]);

const viewLayersParams = useRecoilValue(viewLayersParamsState);

const interactionGroups = useRecoilValue(interactionGroupsState);

const fitBounds = useRecoilValue(mapFitBoundsState);

const resetFitBounds = useResetRecoilState(mapFitBoundsState);
useEffect(() => {
// reset map fit bounds whenever MapView is mounted
resetFitBounds();
}, [resetFitBounds]);

const isMobile = useIsMobile();

return (
<BaseMap mapStyle={mapStyle} viewState={viewState} onViewState={setViewState}>
<DataMap
beforeId={firstLabelId}
viewLayers={viewLayers}
viewLayersParams={viewLayersParams}
interactionGroups={interactionGroups}
/>
<MapBoundsFitter boundingBox={fitBounds} />
<>
<DataMap/>
<MapBoundsFitter />
<DataMapTooltip>
<TooltipContent />
</DataMapTooltip>
{isMobile ? <MapHudMobileLayout /> : <MapHudDesktopLayout />}
</BaseMap>
</>
);
};

export const MapView = () => (
<ErrorBoundary message="There was a problem displaying the map." justifyErrorContent="center">
<Suspense fallback={null}>
<MapViewContent />
<BaseMap>
<MapViewContent />
</BaseMap>
</Suspense>
</ErrorBoundary>
);

0 comments on commit 16a42a0

Please sign in to comment.