Add reuseMaps prop (#1730)

This commit is contained in:
Xiaoji Chen 2022-02-07 16:49:02 -08:00 committed by Xiaoji Chen
parent fe779d77b0
commit f666ef87d2
8 changed files with 244 additions and 5 deletions

View File

@ -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'`

View File

@ -28,6 +28,7 @@ export const MapContext = React.createContext<MapContextValue>(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<MapRef, MapProps>((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<MapRef, MapProps>((props, ref) => {
isMounted = false;
if (mapbox) {
mountedMapsContext?.onMapUnmount(props.id);
mapbox.destroy();
if (props.reuseMaps) {
mapbox.recycle();
} else {
mapbox.destroy();
}
}
};
}, []);

View File

@ -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();
}

View File

@ -0,0 +1,32 @@
<!doctype html>
<html>
<head>
<meta charset='UTF-8' />
<title>react-map-gl Example</title>
<link href="https://api.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.css" rel="stylesheet">
<style>
body {
margin: 0;
font-family: Helvetica, Arial, sans-serif;
overflow: hidden;
}
#app {
width: 100vw;
height: 100vh;
}
button {
position: fixed;
z-index: 1;
left: 0;
top: 0;
}
</style>
</head>
<body>
<div id="app"></div>
<script src='app.js'></script>
</body>
<script type="text/javascript">
App.renderToDom(document.getElementById('app'));
</script>
</html>

View File

@ -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"
}
}

View File

@ -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 && <Map {...CONFIGS[key]} reuseMaps mapboxAccessToken={TOKEN} onLoad={onLoad} />}
<button onClick={onClickBtn}>switch</button>
</>
);
}
export function renderToDom(container) {
render(<App />, container);
}

View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "es2020",
"jsx": "react",
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"sourceMap": true,
"module": "ES2020"
}
}

View File

@ -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;