-
Notifications
You must be signed in to change notification settings - Fork 2.2k
fix(widgets): useWidget cleanup and add StrictMode test #9873
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b9b1295
01fc92f
d6c8f99
4699626
72aa1b3
436bfbc
b031079
0bbb43b
2ad7e8a
be6d1ab
029e022
3d8cbb3
fad89d5
e9a5d03
044fd80
7028b46
4378aeb
36a9cc5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) |
| 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 />); |
| 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> |
| 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" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
||
|
|
@@ -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( | ||
|
|
@@ -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, | ||
| ...props, | ||
| // Override user styling props. We will set the canvas style in render() | ||
| style: null, | ||
|
|
@@ -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 | ||
|
|
@@ -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); | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. onLoad callback never fires in StrictModeHigh Severity The |
||
| return () => { | ||
| if (timeoutId !== undefined) { | ||
| clearTimeout(timeoutId); | ||
| } | ||
| }; | ||
| }, [onLoadPending]); | ||
|
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 | ||
|
|
@@ -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', { | ||
|
|
||


Uh oh!
There was an error while loading. Please reload this page.