From f666ef87d2c31de7b40b094ee4cd892e02cd613f Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Mon, 7 Feb 2022 16:49:02 -0800 Subject: [PATCH] Add reuseMaps prop (#1730) --- docs/api-reference/map.md | 10 +++++ src/components/map.tsx | 15 +++++-- src/mapbox/mapbox.ts | 46 +++++++++++++++++++- test/apps/reuse-maps/index.html | 32 ++++++++++++++ test/apps/reuse-maps/package.json | 23 ++++++++++ test/apps/reuse-maps/src/app.tsx | 59 ++++++++++++++++++++++++++ test/apps/reuse-maps/tsconfig.json | 10 +++++ test/apps/reuse-maps/webpack.config.js | 54 +++++++++++++++++++++++ 8 files changed, 244 insertions(+), 5 deletions(-) create mode 100644 test/apps/reuse-maps/index.html create mode 100644 test/apps/reuse-maps/package.json create mode 100644 test/apps/reuse-maps/src/app.tsx create mode 100644 test/apps/reuse-maps/tsconfig.json create mode 100644 test/apps/reuse-maps/webpack.config.js diff --git a/docs/api-reference/map.md b/docs/api-reference/map.md index 60f4e8b2..97b2b37b 100644 --- a/docs/api-reference/map.md +++ b/docs/api-reference/map.md @@ -634,6 +634,16 @@ Default: `true` If `false`, the map won't attempt to re-request tiles once they expire per their HTTP `cacheControl`/`expires` headers. +#### `reuseMaps`: boolean + +Default: `false` + +By default, every time a map component is unmounted, all internal resources associated with the underlying `Map` instance are released. If the map gets mounted again, a new `Map` instance is constructed. + +If `reuseMaps` is set to `true`, when a map component is unmounted, the underlying `Map` instance is retained in memory. The next time a map component gets mounted, the saved instance is reused. This behavior may be desirable if an application frequently mounts/unmounts map(s), for example in a tabbed or collapsable UI, and wants to avoid [new billable events](https://github.com/mapbox/mapbox-gl-js/releases/tag/v2.0.0) triggered by initialization. + +Note that since some map options cannot be modified after initialization, when reusing maps, only the reactive props and `initialViewState` of the new component are respected. + #### `RTLTextPlugin`: string Default: `'https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.2.3/mapbox-gl-rtl-text.js'` diff --git a/src/components/map.tsx b/src/components/map.tsx index 33663c3b..a9e120ab 100644 --- a/src/components/map.tsx +++ b/src/components/map.tsx @@ -28,6 +28,7 @@ export const MapContext = React.createContext(null); export type MapProps = MapboxProps & GlobalSettings & { mapLib?: any; + reuseMaps?: boolean; /** Map container id */ id?: string; /** Map container CSS style */ @@ -94,8 +95,12 @@ const Map = forwardRef((props, ref) => { if (mapboxgl.supported(props)) { setGlobals(mapboxgl, props); - mapbox = new Mapbox(mapboxgl.Map, props); - mapbox.initialize(containerRef.current); + if (props.reuseMaps) { + mapbox = Mapbox.reuse(props, containerRef.current); + } + if (!mapbox) { + mapbox = new Mapbox(mapboxgl.Map, props, containerRef.current); + } contextValue.map = mapbox.map; contextValue.mapLib = mapboxgl; @@ -118,7 +123,11 @@ const Map = forwardRef((props, ref) => { isMounted = false; if (mapbox) { mountedMapsContext?.onMapUnmount(props.id); - mapbox.destroy(); + if (props.reuseMaps) { + mapbox.recycle(); + } else { + mapbox.destroy(); + } } }; }, []); diff --git a/src/mapbox/mapbox.ts b/src/mapbox/mapbox.ts index 4577831d..4fedfe49 100644 --- a/src/mapbox/mapbox.ts +++ b/src/mapbox/mapbox.ts @@ -429,9 +429,12 @@ export default class Mapbox { rotate: false }; - constructor(MapClass: typeof MapboxMap, props: MapboxProps) { + static savedMaps: Mapbox[] = []; + + constructor(MapClass: typeof MapboxMap, props: MapboxProps, container: HTMLDivElement) { this._MapClass = MapClass; this.props = props; + this._initialize(container); } get map(): MapboxMap { @@ -464,7 +467,42 @@ export default class Mapbox { } } - initialize(container: HTMLDivElement) { + static reuse(props: MapboxProps, container: HTMLDivElement) { + const that = Mapbox.savedMaps.pop(); + if (!that) { + return null; + } + + const map = that.map; + // When reusing the saved map, we need to reparent the map(canvas) and other child nodes + // intoto the new container from the props. + // Step1: reparenting child nodes from old container to new container + const oldContainer = map.getContainer(); + container.className = oldContainer.className; + while (oldContainer.childNodes.length > 0) { + container.appendChild(oldContainer.childNodes[0]); + } + // Step2: replace the internal container with new container from the react component + // @ts-ignore + map._container = container; + + // Step 3: apply new props + if (props.initialViewState) { + that._updateViewState(props.initialViewState, false); + } + map.resize(); + that.setProps({...props, styleDiffing: false}); + + // Simulate load event + if (map.isStyleLoaded()) { + map.fire('load'); + } else { + map.once('styledata', () => map.fire('load')); + } + return that; + } + + _initialize(container: HTMLDivElement) { const {props} = this; const mapOptions = { ...props, @@ -538,6 +576,10 @@ export default class Mapbox { this._map = map; } + recycle() { + Mapbox.savedMaps.push(this); + } + destroy() { this._map.remove(); } diff --git a/test/apps/reuse-maps/index.html b/test/apps/reuse-maps/index.html new file mode 100644 index 00000000..e0b2e998 --- /dev/null +++ b/test/apps/reuse-maps/index.html @@ -0,0 +1,32 @@ + + + + + react-map-gl Example + + + + +
+ + + + diff --git a/test/apps/reuse-maps/package.json b/test/apps/reuse-maps/package.json new file mode 100644 index 00000000..59cbf0c5 --- /dev/null +++ b/test/apps/reuse-maps/package.json @@ -0,0 +1,23 @@ +{ + "scripts": { + "start": "webpack-dev-server --progress --hot --open", + "start-local": "webpack-dev-server --env local --progress --hot --open" + }, + "dependencies": { + "react": "^17.0.0", + "react-dom": "^17.0.0", + "react-map-gl": "^7.0.0", + "mapbox-gl": "^2.0.0" + }, + "devDependencies": { + "@babel/core": "^7.0.0", + "@babel/preset-env": "^7.0.0", + "@babel/preset-react": "^7.0.0", + "babel-loader": "^8.0.0", + "ts-loader": "^9.0.0", + "typescript": "^4.0.0", + "webpack": "^5.65.0", + "webpack-cli": "^4.9.0", + "webpack-dev-server": "^4.7.0" + } +} diff --git a/test/apps/reuse-maps/src/app.tsx b/test/apps/reuse-maps/src/app.tsx new file mode 100644 index 00000000..c13de311 --- /dev/null +++ b/test/apps/reuse-maps/src/app.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import {useState} from 'react'; +import {render} from 'react-dom'; +import Map from 'react-map-gl'; + +const TOKEN = ''; // Set your mapbox token here + +const CONFIGS = [ + { + style: {width: '100%', height: '100%'}, + mapStyle: 'mapbox://styles/mapbox/dark-v9', + initialViewState: { + longitude: -122.4, + latitude: 37.8, + zoom: 12 + } + }, + { + style: {width: 400, height: 300, margin: 100}, + mapStyle: 'mapbox://styles/mapbox/light-v9', + initialViewState: { + longitude: -100, + latitude: 40, + zoom: 3.5 + } + }, + { + style: {width: '50vw', height: '100vh', marginLeft: '50vw'}, + mapStyle: 'mapbox://styles/mapbox/streets-v9', + longitude: -70.4, + latitude: 40.1, + zoom: 6 + } +]; + +export default function App() { + const [key, setKey] = useState(0); + const [showMap, setShowMap] = useState(true); + + const onClickBtn = () => { + if (!showMap) { + setKey((key + 1) % CONFIGS.length); + } + setShowMap(!showMap); + }; + + const onLoad = () => console.log(key, 'loaded'); // eslint-disable-line + + return ( + <> + {showMap && } + + + ); +} + +export function renderToDom(container) { + render(, container); +} diff --git a/test/apps/reuse-maps/tsconfig.json b/test/apps/reuse-maps/tsconfig.json new file mode 100644 index 00000000..e53974cb --- /dev/null +++ b/test/apps/reuse-maps/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es2020", + "jsx": "react", + "allowSyntheticDefaultImports": true, + "moduleResolution": "node", + "sourceMap": true, + "module": "ES2020" + } +} \ No newline at end of file diff --git a/test/apps/reuse-maps/webpack.config.js b/test/apps/reuse-maps/webpack.config.js new file mode 100644 index 00000000..8d53d3e2 --- /dev/null +++ b/test/apps/reuse-maps/webpack.config.js @@ -0,0 +1,54 @@ +// NOTE: To use this example standalone (e.g. outside of repo) +// delete the local development overrides at the bottom of this file + +// avoid destructuring for older Node version support +const resolve = require('path').resolve; +const webpack = require('webpack'); + +const config = { + mode: 'development', + + devServer: { + static: '.' + }, + + entry: { + app: resolve('./src/app') + }, + + output: { + library: 'App' + }, + + resolve: { + extensions: ['.ts', '.tsx', '.js', '.json'] + }, + + module: { + rules: [ + { + test: /\.(ts|js)x?$/, + include: [resolve('.')], + exclude: [/node_modules/], + use: [ + { + loader: 'babel-loader', + options: { + presets: ['@babel/env', '@babel/react'] + } + }, + { + loader: 'ts-loader' + } + ] + } + ] + }, + + // Optional: Enables reading mapbox token from environment variable + plugins: [new webpack.EnvironmentPlugin({MapboxAccessToken: ''})] +}; + +// Enables bundling against src in this repo rather than the installed version +module.exports = env => + env && env.local ? require('../../../examples/webpack.config.local')(config)(env) : config;