Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b9b1295
Fix useWidget cleanup and add StrictMode test
ibgreen Nov 18, 2025
01fc92f
fix
ibgreen Dec 2, 2025
d6c8f99
fix(react): Fix useWidget cleanup in React StrictMode
chrisgervang Jan 29, 2026
4699626
fix(react): Register widget during render for onLoad availability
chrisgervang Jan 29, 2026
72aa1b3
fix(react): Render widget children before deck exists for StrictMode …
chrisgervang Jan 29, 2026
436bfbc
chore: Fix prettier formatting
chrisgervang Jan 29, 2026
b031079
fix(react): Fix useWidget cleanup logic and add test
chrisgervang Feb 2, 2026
0bbb43b
fix(react): Wait for widget to render in StrictMode test
chrisgervang Feb 2, 2026
2ad7e8a
fix(react): Check for user widgets before setProps overwrites them
chrisgervang Feb 2, 2026
be6d1ab
fix(react): Handle StrictMode double-mount in useWidget by finding wi…
chrisgervang Feb 2, 2026
029e022
fix(react): Fix WebGL context exhaustion and nested act() errors in t…
chrisgervang Feb 2, 2026
3d8cbb3
fix(react): Defer onLoad to ensure React widgets are available
chrisgervang Feb 4, 2026
fad89d5
fix(react): Fix StrictMode widget cleanup with deferred removal
chrisgervang Feb 4, 2026
e9a5d03
chore(examples): Add React widgets test app and update get-started ex…
chrisgervang Feb 4, 2026
044fd80
fix(react): Scope pending widget removals per DeckGL instance
chrisgervang Feb 4, 2026
7028b46
docs(examples): Update React widgets README for MapLibre
chrisgervang Feb 4, 2026
4378aeb
fix(react): Add clearTimeout cleanup to deferred onLoad effect
chrisgervang Feb 4, 2026
36a9cc5
Update deckgl.ts
chrisgervang Feb 6, 2026
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
23 changes: 23 additions & 0 deletions examples/get-started/react/widgets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
This is a minimal standalone version of the DeckGL React widgets example
on [deck.gl](http://deck.gl) website.

## Usage

```bash
# install dependencies
npm install
# or
yarn
# bundle and serve the app with vite
npm start
```

## Data format

Sample data is from [Natural Earth](http://www.naturalearthdata.com/) via [geojson.xyz](http://geojson.xyz/).

## Features

This example demonstrates:
- Integrating DeckGL with react-map-gl (MapLibre)
- Using declarative widget components (FullscreenWidget, ZoomWidget, CompassWidget)
86 changes: 86 additions & 0 deletions examples/get-started/react/widgets/app.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import React from 'react';
import {createRoot} from 'react-dom/client';
import {Map} from 'react-map-gl/maplibre';
import {DeckGL, GeoJsonLayer, ArcLayer} from 'deck.gl';
import {FullscreenWidget, ZoomWidget, CompassWidget} from '@deck.gl/react';
import {DarkGlassTheme, LightGlassTheme} from '@deck.gl/widgets';
import '@deck.gl/widgets/stylesheet.css';
import 'maplibre-gl/dist/maplibre-gl.css';

/* global window */
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
const widgetTheme = prefersDarkScheme.matches ? DarkGlassTheme : LightGlassTheme;

// source: Natural Earth http://www.naturalearthdata.com/ via geojson.xyz
const COUNTRIES =
'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_50m_admin_0_scale_rank.geojson'; //eslint-disable-line
const AIR_PORTS =
'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_airports.geojson';

const INITIAL_VIEW_STATE = {
latitude: 51.47,
longitude: 0.45,
zoom: 4,
bearing: 0,
pitch: 30
};

const MAP_STYLE = 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json';

function Root() {
const onClick = info => {
if (info.object) {
// eslint-disable-next-line
alert(`${info.object.properties.name} (${info.object.properties.abbrev})`);
}
};

return (
<DeckGL controller={true} initialViewState={INITIAL_VIEW_STATE}>
<Map mapStyle={MAP_STYLE} />
<GeoJsonLayer
id="base-map"
data={COUNTRIES}
stroked={true}
filled={true}
lineWidthMinPixels={2}
opacity={0.4}
getLineColor={[60, 60, 60]}
getFillColor={[200, 200, 200]}
/>
<GeoJsonLayer
id="airports"
data={AIR_PORTS}
filled={true}
pointRadiusMinPixels={2}
pointRadiusScale={2000}
getPointRadius={f => 11 - f.properties.scalerank}
getFillColor={[200, 0, 80, 180]}
pickable={true}
autoHighlight={true}
onClick={onClick}
/>
<ArcLayer
id="arcs"
data={AIR_PORTS}
dataTransform={d => d.features.filter(f => f.properties.scalerank < 4)}
getSourcePosition={f => [-0.4531566, 51.4709959]}
getTargetPosition={f => f.geometry.coordinates}
getSourceColor={[0, 128, 200]}
getTargetColor={[200, 0, 80]}
getWidth={1}
/>
<FullscreenWidget style={widgetTheme} />
<ZoomWidget style={widgetTheme} />
<CompassWidget style={widgetTheme} />
</DeckGL>
);
}

/* global document */
const container = document.body.appendChild(document.createElement('div'));
createRoot(container).render(<Root />);
13 changes: 13 additions & 0 deletions examples/get-started/react/widgets/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>deck.gl React Widgets Example</title>
<style>
body {margin: 0; width: 100vw; height: 100vh; overflow: hidden;}
</style>
</head>
<body>
</body>
<script type="module" src="app.jsx"></script>
</html>
21 changes: 21 additions & 0 deletions examples/get-started/react/widgets/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "deckgl-example-react-widgets",
"version": "0.0.0",
"private": true,
"license": "MIT",
"scripts": {
"start": "vite --open",
"start-local": "vite --config ../../../vite.config.local.mjs",
"build": "vite build"
},
"dependencies": {
"deck.gl": "^9.0.0",
"maplibre-gl": "^5.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-map-gl": "^8.0.0"
},
"devDependencies": {
"vite": "^4.0.0"
}
}
48 changes: 44 additions & 4 deletions modules/react/src/deckgl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import positionChildrenUnderViews from './utils/position-children-under-views';
import extractStyles from './utils/extract-styles';

import type {DeckGLContextValue} from './utils/deckgl-context';
import type {DeckProps, View, Viewport} from '@deck.gl/core';
import type {DeckProps, View, Viewport, Widget} from '@deck.gl/core';

export type ViewOrViews = View | View[] | null;

Expand Down Expand Up @@ -122,6 +122,11 @@ function DeckGLWithRef<ViewsT extends ViewOrViews = null>(
// DOM refs
const containerRef = useRef(null);
const canvasRef = useRef(null);
// Stable widgets array for React widget components (survives StrictMode remounts)
const widgetsRef = useRef<Widget[]>([]);
// Track deferred onLoad - use state to trigger re-render when deck initializes
const [onLoadPending, setOnLoadPending] = useState(false);
const onLoadCalledRef = useRef(false);

// extract any deck.gl layers masquerading as react elements from props.children
const jsxProps = useMemo(
Expand Down Expand Up @@ -156,12 +161,20 @@ function DeckGLWithRef<ViewsT extends ViewOrViews = null>(
}
};

// Defer onLoad until after React widget children have had a chance to register.
// Deck's onLoad fires during initialization, before React children render.
// By using state to track pending status, we trigger a re-render when deck initializes,
// then call onLoad in useEffect after children have rendered and registered widgets.
const handleOnLoad: DeckProps<ViewsT>['onLoad'] = () => {
setOnLoadPending(true);
};

// Update Deck's props. If Deck needs redraw, this will trigger a call to `_customRender` in
// the next animation frame.
// Needs to be called both from initial mount, and when new props are received
const deckProps = useMemo(() => {
const forwardProps: DeckProps<ViewsT> = {
widgets: [],
widgets: widgetsRef.current,
Comment thread
cursor[bot] marked this conversation as resolved.
...props,
// Override user styling props. We will set the canvas style in render()
style: null,
Expand All @@ -172,7 +185,9 @@ function DeckGLWithRef<ViewsT extends ViewOrViews = null>(
layers: jsxProps.layers,
views: jsxProps.views as ViewsT,
onViewStateChange: handleViewStateChange,
onInteractionStateChange: handleInteractionStateChange
onInteractionStateChange: handleInteractionStateChange,
// The deferred effect will only call the user's callback if they provided one.
onLoad: handleOnLoad
};

// The defaultValue for _customRender is null, which would overwrite the definition
Expand All @@ -198,6 +213,30 @@ function DeckGLWithRef<ViewsT extends ViewOrViews = null>(
return () => thisRef.deck?.finalize();
}, []);

// Stable reference to onLoad callback for use in deferred effect
const onLoadRef = useRef(props.onLoad);
onLoadRef.current = props.onLoad;

// Call deferred onLoad after React widget children have registered.
// React guarantees parent effects run after children effects, so by this point
// any widgets using useWidget will have synced to deck.
// Use setTimeout(0) to escape React's commit phase and act() scope, allowing
// the callback to safely trigger state updates or nested act() calls in tests.
useEffect(() => {
let timeoutId: ReturnType<typeof setTimeout> | undefined;
if (onLoadPending && !onLoadCalledRef.current) {
onLoadCalledRef.current = true;
timeoutId = setTimeout(() => {
onLoadRef.current?.();
}, 0);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onLoad callback never fires in StrictMode

High Severity

The onLoadCalledRef.current flag is set to true before the setTimeout callback actually fires. In React StrictMode, effects run twice (mount → cleanup → remount). During cleanup, the timeout is cancelled via clearTimeout, but onLoadCalledRef is never reset. When the effect re-runs, the condition !onLoadCalledRef.current is false, so no new timeout is scheduled. This means the user's onLoad callback is never invoked in StrictMode.

Fix in Cursor Fix in Web

return () => {
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
}
};
}, [onLoadPending]);
Comment thread
cursor[bot] marked this conversation as resolved.

useIsomorphicLayoutEffect(() => {
// render has just been called. The children are positioned based on the current view state.
// Redraw Deck canvas immediately, if necessary, using the current view state, so that it
Expand Down Expand Up @@ -249,7 +288,8 @@ function DeckGLWithRef<ViewsT extends ViewOrViews = null>(
const childrenUnderViews = positionChildrenUnderViews({
children: jsxProps.children,
deck: thisRef.deck,
ContextProvider
ContextProvider,
widgets: widgetsRef.current
});

const canvas = createElement('canvas', {
Expand Down
8 changes: 5 additions & 3 deletions modules/react/src/utils/position-children-under-views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,21 @@ import {inheritsFrom} from './inherits-from';
import evaluateChildren, {isComponent} from './evaluate-children';

import type {ViewOrViews} from '../deckgl';
import type {Deck, Viewport} from '@deck.gl/core';
import type {Deck, Viewport, Widget} from '@deck.gl/core';
import {DeckGlContext, type DeckGLContextValue} from './deckgl-context';

// Iterate over views and reposition children associated with views
// TODO - Can we supply a similar function for the non-React case?
export default function positionChildrenUnderViews<ViewsT extends ViewOrViews>({
children,
deck,
ContextProvider = DeckGlContext.Provider
ContextProvider = DeckGlContext.Provider,
widgets
}: {
children: React.ReactNode[];
deck?: Deck<ViewsT>;
ContextProvider?: React.Context<DeckGLContextValue>['Provider'];
widgets: Widget[];
}): React.ReactNode[] {
// @ts-expect-error accessing protected property
const {viewManager} = deck || {};
Expand Down Expand Up @@ -106,7 +108,7 @@ export default function positionChildrenUnderViews<ViewsT extends ViewOrViews>({
// @ts-expect-error accessing protected method
deck._onViewStateChange(params);
},
widgets: []
widgets
};
const providerKey = `view-${viewId}-context`;
return createElement(ContextProvider, {key: providerKey, value: contextValue}, viewElement);
Expand Down
Loading
Loading