Skip to content

Commit

Permalink
Merge branch 'map-state-management'
Browse files Browse the repository at this point in the history
* map-state-management:
  Updated map loading spinner
  Simplify map state managment with Zustand
  • Loading branch information
davenquinn committed Nov 16, 2024
2 parents f61f81a + 0bc1a71 commit d7e8261
Show file tree
Hide file tree
Showing 9 changed files with 93 additions and 65 deletions.
6 changes: 6 additions & 0 deletions packages/map-interface/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions packages/map-interface/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
Expand Down
5 changes: 2 additions & 3 deletions packages/map-interface/src/context-panel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 4 additions & 3 deletions packages/map-interface/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
useMapEaseTo,
useMapDispatch,
useMapStatus,
useMapInitialized,
} from "@macrostrat/mapbox-react";
import { useMemo, useRef } from "react";
import { debounce } from "underscore";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down
5 changes: 4 additions & 1 deletion packages/mapbox-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ 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
- Add some stories for testing
- 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
Expand Down
5 changes: 3 additions & 2 deletions packages/mapbox-react/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
Expand Down
117 changes: 68 additions & 49 deletions packages/mapbox-react/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Map | null>(null);
const [store] = useState(() => {
return createStore<MapState>((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<T>(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<Map | null>;
dispatch(action: MapAction): void;
}

interface MapStatus {
isLoading: boolean;
Expand All @@ -29,33 +68,45 @@ const defaultMapStatus: MapStatus = {
isStyleLoaded: false,
};

const MapDispatchContext = createContext<React.Dispatch<MapAction>>(null);
const MapRefContext = createContext<RefObject<Map | null>>(null);
const MapStatusContext = createContext<MapStatus>(defaultMapStatus);
const MapPositionContext = createContext<MapPosition>(null);

export function useMapRef() {
return useContext(MapRefContext);
return useMapStore((state) => state.ref);
}

export function useMapStatus() {
return useContext(MapStatusContext);
export function useMapStatus(
selector: (state: MapStatus) => any | null = null
) {
return useMapStore(useSubSelector("status", selector));
}

export function useMapInitialized() {
return useMapStore((state) => state.status.isInitialized);
}

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 =
Expand All @@ -65,7 +116,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, {
Expand All @@ -87,35 +138,3 @@ function mapReducer(state: MapCtx, action: MapAction): MapCtx {
return { ...state, position: action.payload };
}
}

export function MapboxMapProvider({ children }) {
const mapRef = useRef<Map | null>();
const [value, _dispatch] = useReducer<Reducer<MapCtx, MapAction>>(
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)
)
)
);
}
6 changes: 3 additions & 3 deletions packages/mapbox-react/src/focus-state.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -160,7 +160,7 @@ export function useMapEaseTo(props: MapEaseToProps) {
* controlled outside of the component. */
const updateQueue = useRef<MapEaseToState[]>([]);
// This forces a re-render after initialization, I guess
const { isInitialized } = useMapStatus();
const isInitialized = useMapInitialized();

/** Handle changes to any map props */
useEffect(() => {
Expand Down Expand Up @@ -345,7 +345,7 @@ export function useFocusState(
) {
const map = useMapRef();
const [focusState, setFocusState] = useState<PositionFocusState | null>(null);
const { isInitialized } = useMapStatus();
const isInitialized = useMapInitialized();

useEffect(() => {
if (map.current == null || position == null) return;
Expand Down
3 changes: 1 addition & 2 deletions packages/mapbox-react/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ 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(
operator: (map: mapboxgl.Map) => void,
dependencies: any[] = []
) {
const mapRef = useMapRef();
const { isStyleLoaded } = useMapStatus();
const isStyleLoaded = useMapStatus((s) => s.isStyleLoaded);
useEffect(() => {
const map = mapRef.current;
if (map == null) return;
Expand Down

0 comments on commit d7e8261

Please sign in to comment.