From 7a1052a2dd2b26999751402b9fa3b78ef87c7940 Mon Sep 17 00:00:00 2001 From: Jim O'Donnell Date: Tue, 25 Jun 2024 11:05:14 +0100 Subject: [PATCH] refactor(frontend): concurrent rendering in React 18 (#42) - 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. --- .../adaptations/FeatureAdaptationsTable.tsx | 2 +- frontend/src/index.tsx | 5 +- frontend/src/lib/data-map/BaseMap.tsx | 23 ++++++-- frontend/src/lib/data-map/DataMap.tsx | 40 ++++++++----- frontend/src/lib/map/MapBoundsFitter.tsx | 17 ++++-- frontend/src/map/MapView.tsx | 58 ++----------------- 6 files changed, 64 insertions(+), 81 deletions(-) diff --git a/frontend/src/details/adaptations/FeatureAdaptationsTable.tsx b/frontend/src/details/adaptations/FeatureAdaptationsTable.tsx index ff525533..8adfb068 100644 --- a/frontend/src/details/adaptations/FeatureAdaptationsTable.tsx +++ b/frontend/src/details/adaptations/FeatureAdaptationsTable.tsx @@ -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'; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 46b0f12b..ffc54a78 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,5 +1,6 @@ -import { render } from 'react-dom'; +import { createRoot } from 'react-dom/client'; import { App } from './App'; const container = document.getElementById('root'); -render(, container); +const root = createRoot(container); +root.render(); diff --git a/frontend/src/lib/data-map/BaseMap.tsx b/frontend/src/lib/data-map/BaseMap.tsx index 93a98096..0ecaf88a 100644 --- a/frontend/src/lib/data-map/BaseMap.tsx +++ b/frontend/src/lib/data-map/BaseMap.tsx @@ -1,11 +1,13 @@ 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['mapStyle']; - viewState: MapViewState; - onViewState: (vs: MapViewState) => void; children?: ReactNode; } @@ -13,13 +15,22 @@ export interface BaseMapProps { * Displays a react-map-gl basemap component. * Accepts children such as a DeckGLOverlay, HUD controls etc */ -export const BaseMap: FC = ({ mapStyle, viewState, onViewState, children }) => { +export const BaseMap: FC = ({ 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 ( onViewState(viewState)} + onMove={handleViewStateChange} mapStyle={mapStyle} dragRotate={false} keyboard={false} diff --git a/frontend/src/lib/data-map/DataMap.tsx b/frontend/src/lib/data-map/DataMap.tsx index b4420236..c4e4ca5e 100644 --- a/frontend/src/lib/data-map/DataMap.tsx +++ b/frontend/src/lib/data-map/DataMap.tsx @@ -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'; @@ -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; - 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 = ({ - beforeId, - viewLayers, - viewLayersParams, - interactionGroups, -}) => { +export const DataMap: FC = () => { const deckRef = useRef(); 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( () => @@ -45,9 +53,9 @@ export const DataMap: FC = ({ 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( diff --git a/frontend/src/lib/map/MapBoundsFitter.tsx b/frontend/src/lib/map/MapBoundsFitter.tsx index def3d017..2288cea9 100644 --- a/frontend/src/lib/map/MapBoundsFitter.tsx +++ b/frontend/src/lib/map/MapBoundsFitter.tsx @@ -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({ + key: 'mapFitBoundsState', + default: null, +}); -export const MapBoundsFitter: FC = ({ 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) { diff --git a/frontend/src/map/MapView.tsx b/frontend/src/map/MapView.tsx index 5c67c286..71657d87 100644 --- a/frontend/src/map/MapView.tsx +++ b/frontend/src/map/MapView.tsx @@ -1,18 +1,10 @@ -import { Suspense, useCallback, useEffect } from 'react'; -import { - atom, - useRecoilState, - useRecoilValue, - useResetRecoilState, - useSetRecoilState, -} from 'recoil'; +import { Suspense, useCallback } from 'react'; +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 { @@ -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({ - key: 'mapFitBoundsState', - default: null, -}); const AppPlaceSearch = () => { const setFitBounds = useSetRecoilState(mapFitBoundsState); @@ -114,40 +96,12 @@ 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 ( - - - + + +