Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions modules/react-mapbox/src/components/layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,19 @@ export function Layer(props: LayerProps) {
useEffect(() => {
if (map) {
const forceUpdate = () => setStyleLoaded(version => version + 1);
map.on('styledata', forceUpdate);
// `styledata` fires on every style change, e.g. addImage or setPaintProperty.
// Only re-render if the layer is missing, e.g. after the style was swapped
const onStyleData = () => {
// @ts-ignore
if (!(map.style && map.style._loaded && map.getLayer(id))) {
forceUpdate();
}
};
map.on('styledata', onStyleData);
forceUpdate();

return () => {
map.off('styledata', forceUpdate);
map.off('styledata', onStyleData);
// @ts-ignore
if (map.style && map.style._loaded && map.getLayer(id)) {
map.removeLayer(id);
Expand Down
16 changes: 14 additions & 2 deletions modules/react-mapbox/src/components/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,29 @@ export function Source(props: SourceProps) {
const map = useContext(MapContext).map.getMap();
const propsRef = useRef(props);
const [, setStyleLoaded] = useState(0);
const sourceRef = useRef(null);

const id = useMemo(() => props.id || `jsx-source-${sourceCounter++}`, []);

useEffect(() => {
if (map) {
/* global setTimeout */
const forceUpdate = () => setTimeout(() => setStyleLoaded(version => version + 1), 0);
map.on('styledata', forceUpdate);
// `styledata` fires on every style change, e.g. addImage or setPaintProperty.
// Only re-render if the source is missing or not the one children were rendered with,
// e.g. after the style was swapped or when the style defines a source with the same id
const onStyleData = () => {
// @ts-ignore
const source = map.style && map.style._loaded && map.getSource(id);
if (!source || source !== sourceRef.current) {
forceUpdate();
}
};
map.on('styledata', onStyleData);
forceUpdate();

return () => {
map.off('styledata', forceUpdate);
map.off('styledata', onStyleData);
// @ts-ignore
if (map.style && map.style._loaded && map.getSource(id)) {
// Parent effects are destroyed before child ones, see
Expand Down Expand Up @@ -124,6 +135,7 @@ export function Source(props: SourceProps) {
source = createSource(map, id, props);
}
propsRef.current = props;
sourceRef.current = source;

return (
(source &&
Expand Down
95 changes: 95 additions & 0 deletions modules/react-mapbox/test/components/layer.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,98 @@ test('Source/Layer', async () => {

await act(() => root.unmount());
});

test('Source/Layer do not re-render on unrelated style changes', async () => {
const root = createRoot(document.createElement('div'));
const mapRef = {current: null};
let renderCount = 0;
const onRender = () => renderCount++;

const geoJSON = {
type: 'Point',
coordinates: [0, 0]
};
const pointLayer = {
type: 'circle',
paint: {
'circle-radius': 10,
'circle-color': '#007cbf'
}
};

await act(() =>
root.render(
<Map ref={mapRef} mapLib={import('mapbox-gl-v3')} mapboxAccessToken={MapboxAccessToken}>
<React.Profiler id="layers" onRender={onRender}>
<Source id="my-data" type="geojson" data={geoJSON}>
<Layer id="my-layer" {...pointLayer} />
</Source>
</React.Profiler>
</Map>
)
);
await waitForMapLoad(mapRef);
const map = mapRef.current.getMap();

renderCount = 0;
map.addImage('my-image', {width: 1, height: 1, data: new Uint8Array(4)});
map.setPaintProperty('my-layer', 'circle-color', '#ff0000');
await actUntil(resolveTest => map.once('idle', () => setTimeout(resolveTest, 0)));
expect(renderCount, 'Source and Layer are not rendered again').toBe(0);

map.removeLayer('my-layer');
await actUntil(resolveTest => {
const onStyleData = () => {
if (map.getLayer('my-layer')) {
map.off('styledata', onStyleData);
resolveTest();
}
};
map.on('styledata', onStyleData);
});
expect(map.getLayer('my-layer'), 'Missing layer is added again').toBeTruthy();

await act(() => root.unmount());
});

test('Source renders its children when the style already has its source', async () => {
const root = createRoot(document.createElement('div'));
const mapRef = {current: null};

const geoJSON = {
type: 'Point',
coordinates: [0, 0]
};
const mapStyle = {
version: 8,
sources: {'my-data': {type: 'geojson', data: geoJSON}},
layers: []
};
const pointLayer = {
type: 'circle',
paint: {
'circle-radius': 10,
'circle-color': '#007cbf'
}
};

await act(() =>
root.render(
<Map
ref={mapRef}
mapLib={import('mapbox-gl-v3')}
mapboxAccessToken={MapboxAccessToken}
mapStyle={mapStyle}
>
<Source id="my-data" type="geojson" data={geoJSON}>
<Layer id="my-layer" {...pointLayer} />
</Source>
</Map>
)
);
await waitForMapLoad(mapRef);
await actUntil(resolveTest => setTimeout(resolveTest, 0));
expect(mapRef.current.getLayer('my-layer'), 'Layer is added').toBeTruthy();

await act(() => root.unmount());
});
12 changes: 10 additions & 2 deletions modules/react-maplibre/src/components/layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,19 @@ export function Layer(props: LayerProps) {
useEffect(() => {
if (map) {
const forceUpdate = () => setStyleLoaded(version => version + 1);
map.on('styledata', forceUpdate);
// `styledata` fires on every style change, e.g. addImage or setPaintProperty.
// Only re-render if the layer is missing, e.g. after the style was swapped
const onStyleData = () => {
// @ts-ignore
if (!(map.style && map.style._loaded && map.getLayer(id))) {
forceUpdate();
}
};
map.on('styledata', onStyleData);
forceUpdate();

return () => {
map.off('styledata', forceUpdate);
map.off('styledata', onStyleData);
// @ts-ignore
if (map.style && map.style._loaded && map.getLayer(id)) {
map.removeLayer(id);
Expand Down
16 changes: 14 additions & 2 deletions modules/react-maplibre/src/components/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,18 +86,29 @@ export function Source(props: SourceProps) {
const map = useContext(MapContext).map.getMap();
const propsRef = useRef(props);
const [, setStyleLoaded] = useState(0);
const sourceRef = useRef(null);

const id = useMemo(() => props.id || `jsx-source-${sourceCounter++}`, []);

useEffect(() => {
if (map) {
/* global setTimeout */
const forceUpdate = () => setTimeout(() => setStyleLoaded(version => version + 1), 0);
map.on('styledata', forceUpdate);
// `styledata` fires on every style change, e.g. addImage or setPaintProperty.
// Only re-render if the source is missing or not the one children were rendered with,
// e.g. after the style was swapped or when the style defines a source with the same id
const onStyleData = () => {
// @ts-ignore
const source = map.style && map.style._loaded && map.getSource(id);
if (!source || source !== sourceRef.current) {
forceUpdate();
}
Comment thread
cursor[bot] marked this conversation as resolved.
};
map.on('styledata', onStyleData);
forceUpdate();

return () => {
map.off('styledata', forceUpdate);
map.off('styledata', onStyleData);
// @ts-ignore
if (map.style && map.style._loaded && map.getSource(id)) {
// Parent effects are destroyed before child ones, see
Expand Down Expand Up @@ -127,6 +138,7 @@ export function Source(props: SourceProps) {
source = createSource(map, id, props);
}
propsRef.current = props;
sourceRef.current = source;

return (
(source &&
Expand Down
92 changes: 91 additions & 1 deletion modules/react-maplibre/test/components/layer.spec.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* global document */
/* global document, setTimeout */
import {expect, test} from 'vitest';
import * as React from 'react';
import {createRoot} from 'react-dom/client';
Expand Down Expand Up @@ -76,3 +76,93 @@ test('Source/Layer', async () => {

await act(() => root.unmount());
});

test('Source/Layer do not re-render on unrelated style changes', async () => {
const root = createRoot(document.createElement('div'));
const mapRef = {current: null};
let renderCount = 0;
const onRender = () => renderCount++;

const geoJSON = {
type: 'Point',
coordinates: [0, 0]
};
const pointLayer = {
type: 'circle',
paint: {
'circle-radius': 10,
'circle-color': '#007cbf'
}
};

await act(() =>
root.render(
<Map ref={mapRef}>
<React.Profiler id="layers" onRender={onRender}>
<Source id="my-data" type="geojson" data={geoJSON}>
<Layer id="my-layer" {...pointLayer} />
</Source>
</React.Profiler>
</Map>
)
);
await waitForMapLoad(mapRef);
const map = mapRef.current.getMap();

renderCount = 0;
map.addImage('my-image', {width: 1, height: 1, data: new Uint8Array(4)});
map.setPaintProperty('my-layer', 'circle-color', '#ff0000');
await actUntil(resolveTest => map.once('idle', () => setTimeout(resolveTest, 0)));
expect(renderCount, 'Source and Layer are not rendered again').toBe(0);

map.removeLayer('my-layer');
await actUntil(resolveTest => {
const onStyleData = () => {
if (map.getLayer('my-layer')) {
map.off('styledata', onStyleData);
resolveTest();
}
};
map.on('styledata', onStyleData);
});
expect(map.getLayer('my-layer'), 'Missing layer is added again').toBeTruthy();

await act(() => root.unmount());
});

test('Source renders its children when the style already has its source', async () => {
const root = createRoot(document.createElement('div'));
const mapRef = {current: null};

const geoJSON = {
type: 'Point',
coordinates: [0, 0]
};
const mapStyle = {
version: 8,
sources: {'my-data': {type: 'geojson', data: geoJSON}},
layers: []
};
const pointLayer = {
type: 'circle',
paint: {
'circle-radius': 10,
'circle-color': '#007cbf'
}
};

await act(() =>
root.render(
<Map ref={mapRef} mapStyle={mapStyle}>
<Source id="my-data" type="geojson" data={geoJSON}>
<Layer id="my-layer" {...pointLayer} />
</Source>
</Map>
)
);
await waitForMapLoad(mapRef);
await actUntil(resolveTest => setTimeout(resolveTest, 0));
expect(mapRef.current.getLayer('my-layer'), 'Layer is added').toBeTruthy();

await act(() => root.unmount());
});