From 3623b173b186346fafd3b1ff00561604c1aa50bd Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Sat, 16 Nov 2024 05:38:45 -0600 Subject: [PATCH 1/2] Simplify map state managment with Zustand --- packages/mapbox-react/CHANGELOG.md | 5 +- packages/mapbox-react/package.json | 5 +- packages/mapbox-react/src/context.ts | 113 +++++++++++++++------------ 3 files changed, 71 insertions(+), 52 deletions(-) diff --git a/packages/mapbox-react/CHANGELOG.md b/packages/mapbox-react/CHANGELOG.md index b8c53134..5ac3d7f8 100644 --- a/packages/mapbox-react/CHANGELOG.md +++ b/packages/mapbox-react/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.4.0] - 2024-11-16 + +- Improve state management using a `zustand` store + ## [2.3.0] - 2024-11-05 - Improve the internal design of the `useMapEaseTo` hook @@ -11,7 +15,6 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Added deprecation warnings to `useMapEaseToCenter` and `useMapEaseToBounds` - Add a `useBasicStylePair` hook for getting a basemap in dark or light mode - ## [2.2.3] - 2024-10-24 - Added package specifier for types diff --git a/packages/mapbox-react/package.json b/packages/mapbox-react/package.json index 418d1c44..e81e8654 100644 --- a/packages/mapbox-react/package.json +++ b/packages/mapbox-react/package.json @@ -1,6 +1,6 @@ { "name": "@macrostrat/mapbox-react", - "version": "2.3.0", + "version": "2.4.0", "description": "Components to support using Mapbox maps in React", "main": "dist/main.js", "module": "dist/module.js", @@ -19,7 +19,8 @@ "classnames": "^2.3.1", "immutability-helper": "^3.1.1", "mapbox-gl": "^2.15.0", - "mapbox-gl-controls": "^2.3.5" + "mapbox-gl-controls": "^2.3.5", + "zustand": "^5.0.1" }, "peerDependencies": { "@blueprintjs/core": "^3||^4||^5.10.2", diff --git a/packages/mapbox-react/src/context.ts b/packages/mapbox-react/src/context.ts index 3cc9fcea..62307226 100644 --- a/packages/mapbox-react/src/context.ts +++ b/packages/mapbox-react/src/context.ts @@ -3,14 +3,53 @@ import { useContext, RefObject, useRef, - useReducer, - useCallback, - Reducer, + useState, + useMemo, } from "react"; import update from "immutability-helper"; import { Map } from "mapbox-gl"; import h from "@macrostrat/hyper"; import { MapPosition } from "@macrostrat/mapbox-utils"; +import { createStore, useStore } from "zustand"; + +const MapStoreContext = createContext(null); + +export function MapboxMapProvider({ children }) { + const ref = useRef(null); + const [store] = useState(() => { + return createStore((set) => { + return { + status: defaultMapStatus, + position: null, + // Hold a reference to the map object in state + ref, + dispatch: (action: MapAction): void => { + if (action.type === "set-map") { + ref.current = action.payload; + } + set((state) => mapReducer(state, action)); + }, + }; + }); + }); + + return h(MapStoreContext.Provider, { value: store }, children); +} + +function useMapStore(selector: (state: MapState) => T): T { + const store = useContext(MapStoreContext); + if (!store) { + throw new Error("Missing MapStoreProvider"); + } + return useStore(store, selector); +} + +interface MapState { + status: MapStatus; + position: MapPosition; + ref: RefObject; + dispatch(action: MapAction): void; +} interface MapStatus { isLoading: boolean; @@ -29,33 +68,41 @@ const defaultMapStatus: MapStatus = { isStyleLoaded: false, }; -const MapDispatchContext = createContext>(null); -const MapRefContext = createContext>(null); -const MapStatusContext = createContext(defaultMapStatus); -const MapPositionContext = createContext(null); - export function useMapRef() { - return useContext(MapRefContext); + return useMapStore((state) => state.ref); +} + +export function useMapStatus( + selector: (state: MapStatus) => any | null = null +) { + return useMapStore(useSubSelector("status", selector)); } -export function useMapStatus() { - return useContext(MapStatusContext); +function useSubSelector( + key: string, + selector: (state: any) => any | null +): (state: MapState) => any { + return useMemo(() => { + if (selector == null) { + return (state: MapState) => state[key]; + } else { + return (state: MapState) => selector(state[key]); + } + }, [selector]); } export function useMapPosition() { - return useContext(MapPositionContext); + return useMapStore((state) => state.position); } export function useMapElement(): Map | null { return useMapRef().current; } -export function useMap(): Map | null { - return useMapRef().current; -} +export const useMap = useMapElement; export function useMapDispatch() { - return useContext(MapDispatchContext); + return useMapStore((state) => state.dispatch); } type MapAction = @@ -65,7 +112,7 @@ type MapAction = | { type: "map-moved"; payload: MapPosition } | { type: "set-map"; payload: Map }; -function mapReducer(state: MapCtx, action: MapAction): MapCtx { +function mapReducer(state: MapState, action: MapAction): MapCtx { switch (action.type) { case "set-map": return update(state, { @@ -87,35 +134,3 @@ function mapReducer(state: MapCtx, action: MapAction): MapCtx { return { ...state, position: action.payload }; } } - -export function MapboxMapProvider({ children }) { - const mapRef = useRef(); - const [value, _dispatch] = useReducer>( - mapReducer, - { - status: defaultMapStatus, - position: null, - } - ); - - const dispatch = useCallback((action: MapAction) => { - if (action.type === "set-map") { - mapRef.current = action.payload; - } - _dispatch(action); - }, []); - - return h( - MapDispatchContext.Provider, - { value: dispatch }, - h( - MapRefContext.Provider, - { value: mapRef }, - h( - MapStatusContext.Provider, - { value: value.status }, - h(MapPositionContext.Provider, { value: value.position }, children) - ) - ) - ); -} From 0bc1a7187151728b3321f291cb0d4ce4e8487675 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Sat, 16 Nov 2024 06:01:43 -0600 Subject: [PATCH 2/2] Updated map loading spinner --- packages/map-interface/CHANGELOG.md | 6 ++++++ packages/map-interface/package.json | 4 ++-- packages/map-interface/src/context-panel/index.ts | 5 ++--- packages/map-interface/src/helpers.ts | 7 ++++--- packages/mapbox-react/src/context.ts | 4 ++++ packages/mapbox-react/src/focus-state.ts | 6 +++--- packages/mapbox-react/src/hooks.ts | 3 +-- 7 files changed, 22 insertions(+), 13 deletions(-) diff --git a/packages/map-interface/CHANGELOG.md b/packages/map-interface/CHANGELOG.md index aa784b23..ab29e8b1 100644 --- a/packages/map-interface/CHANGELOG.md +++ b/packages/map-interface/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.0] - 2024-11-16 + +- Improve map state management with `zustand` (in `@macrostrat/mapbox-react`) +- Add `styleType` prop to `DevMapPage` component to allow setting "standard" + Mapbox styles or "macrostrat" styles (the default) + ## [1.0.12] - 2024-11-13 - Add a `bounds` option to the `DevMapPage` component diff --git a/packages/map-interface/package.json b/packages/map-interface/package.json index dc095e81..f4e5ddee 100644 --- a/packages/map-interface/package.json +++ b/packages/map-interface/package.json @@ -1,6 +1,6 @@ { "name": "@macrostrat/map-interface", - "version": "1.0.12", + "version": "1.1.0", "description": "Map interface for Macrostrat", "main": "dist/index.cjs.js", "module": "dist/index.js", @@ -10,7 +10,7 @@ "dependencies": { "@macrostrat/color-utils": "^1.0.0", "@macrostrat/hyper": "^3.0.0", - "@macrostrat/mapbox-react": "^2.2.3", + "@macrostrat/mapbox-react": "^2.4.0", "@macrostrat/mapbox-utils": "^1.3.2", "@macrostrat/ui-components": "^4.0.4", "@mapbox/tilebelt": "^2.0.0", diff --git a/packages/map-interface/src/context-panel/index.ts b/packages/map-interface/src/context-panel/index.ts index 0d1290df..67eae061 100644 --- a/packages/map-interface/src/context-panel/index.ts +++ b/packages/map-interface/src/context-panel/index.ts @@ -29,9 +29,8 @@ export function LoadingButton({ } export function MapLoadingButton(props) { - const { isLoading } = useMapStatus(); - const mapIsLoading = useMemo(() => isLoading, [isLoading]); - return h(LoadingButton, { ...props, isLoading: mapIsLoading }); + const isLoading = useMapStatus((s) => s.isLoading); + return h(LoadingButton, { ...props, isLoading }); } type AnyChildren = React.ReactNode; diff --git a/packages/map-interface/src/helpers.ts b/packages/map-interface/src/helpers.ts index 5014a61d..5aa9d4c4 100644 --- a/packages/map-interface/src/helpers.ts +++ b/packages/map-interface/src/helpers.ts @@ -3,6 +3,7 @@ import { useMapEaseTo, useMapDispatch, useMapStatus, + useMapInitialized, } from "@macrostrat/mapbox-react"; import { useMemo, useRef } from "react"; import { debounce } from "underscore"; @@ -86,7 +87,7 @@ export function MapPaddingManager({ export function MapMovedReporter({ onMapMoved = null }) { const mapRef = useMapRef(); const dispatch = useMapDispatch(); - const { isInitialized } = useMapStatus(); + const isInitialized = useMapInitialized(); const mapMovedCallback = useCallback(() => { const map = mapRef.current; @@ -121,7 +122,7 @@ export function MapLoadingReporter({ const mapRef = useMapRef(); const loadingRef = useRef(false); const dispatch = useMapDispatch(); - const { isInitialized } = useMapStatus(); + const isInitialized = useMapInitialized(); useEffect(() => { const map = mapRef.current; @@ -157,7 +158,7 @@ export function MapLoadingReporter({ export function MapMarker({ position, setPosition, centerMarker = true }) { const mapRef = useMapRef(); const markerRef = useRef(null); - const { isInitialized } = useMapStatus(); + const isInitialized = useMapInitialized(); useMapMarker(mapRef, markerRef, position); diff --git a/packages/mapbox-react/src/context.ts b/packages/mapbox-react/src/context.ts index 62307226..1b31cede 100644 --- a/packages/mapbox-react/src/context.ts +++ b/packages/mapbox-react/src/context.ts @@ -78,6 +78,10 @@ export function useMapStatus( return useMapStore(useSubSelector("status", selector)); } +export function useMapInitialized() { + return useMapStore((state) => state.status.isInitialized); +} + function useSubSelector( key: string, selector: (state: any) => any | null diff --git a/packages/mapbox-react/src/focus-state.ts b/packages/mapbox-react/src/focus-state.ts index cffab7ed..4417ef4b 100644 --- a/packages/mapbox-react/src/focus-state.ts +++ b/packages/mapbox-react/src/focus-state.ts @@ -1,6 +1,6 @@ /* Reporters and buttons for evaluating a feature's focus on the map. */ import { Intent, Button } from "@blueprintjs/core"; -import { useMapRef, useMapStatus } from "./context"; +import { useMapInitialized, useMapRef, useMapStatus } from "./context"; import classNames from "classnames"; import { useState, useRef, useEffect } from "react"; import bbox from "@turf/bbox"; @@ -160,7 +160,7 @@ export function useMapEaseTo(props: MapEaseToProps) { * controlled outside of the component. */ const updateQueue = useRef([]); // This forces a re-render after initialization, I guess - const { isInitialized } = useMapStatus(); + const isInitialized = useMapInitialized(); /** Handle changes to any map props */ useEffect(() => { @@ -345,7 +345,7 @@ export function useFocusState( ) { const map = useMapRef(); const [focusState, setFocusState] = useState(null); - const { isInitialized } = useMapStatus(); + const isInitialized = useMapInitialized(); useEffect(() => { if (map.current == null || position == null) return; diff --git a/packages/mapbox-react/src/hooks.ts b/packages/mapbox-react/src/hooks.ts index a466b55d..f6f8e667 100644 --- a/packages/mapbox-react/src/hooks.ts +++ b/packages/mapbox-react/src/hooks.ts @@ -3,7 +3,6 @@ import mapboxgl from "mapbox-gl"; import { toggleMapLabelVisibility } from "@macrostrat/mapbox-utils"; import { useMapRef, useMapStatus } from "./context"; import { useCallback } from "react"; -import { useInDarkMode } from "@macrostrat/ui-components"; /** A newer and more flexible version of useMapConditionalStyle */ export function useMapStyleOperator( @@ -11,7 +10,7 @@ export function useMapStyleOperator( dependencies: any[] = [] ) { const mapRef = useMapRef(); - const { isStyleLoaded } = useMapStatus(); + const isStyleLoaded = useMapStatus((s) => s.isStyleLoaded); useEffect(() => { const map = mapRef.current; if (map == null) return;