UNPKG

@deck.gl/react

Version:

React Components for deck.gl

183 lines 8.68 kB
// deck.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import * as React from 'react'; import { createElement, useRef, useState, useMemo, useEffect, useImperativeHandle } from 'react'; import { Deck } from '@deck.gl/core'; import useIsomorphicLayoutEffect from "./utils/use-isomorphic-layout-effect.js"; import extractJSXLayers from "./utils/extract-jsx-layers.js"; import positionChildrenUnderViews from "./utils/position-children-under-views.js"; import extractStyles from "./utils/extract-styles.js"; function getRefHandles(thisRef) { return { get deck() { return thisRef.deck; }, // The following method can only be called after ref is available, by which point deck is defined in useEffect pickObject: opts => thisRef.deck.pickObject(opts), pickMultipleObjects: opts => thisRef.deck.pickMultipleObjects(opts), pickObjects: opts => thisRef.deck.pickObjects(opts) }; } function redrawDeck(thisRef) { if (thisRef.redrawReason) { // Only redraw if we have received a dirty flag // @ts-expect-error accessing protected method thisRef.deck._drawLayers(thisRef.redrawReason); thisRef.redrawReason = null; } } function createDeckInstance(thisRef, DeckClass, props) { const deck = new DeckClass({ ...props, // The Deck's animation loop is independent from React's render cycle, causing potential // synchronization issues. We provide this custom render function to make sure that React // and Deck update on the same schedule. // TODO(ibgreen) - Hack to enable WebGPU as it needs to render quickly to avoid CanvasContext texture from going stale _customRender: props.deviceProps?.adapters?.[0]?.type === 'webgpu' ? undefined : redrawReason => { // Save the dirty flag for later thisRef.redrawReason = redrawReason; // Viewport/view state is passed to child components as props. // If they have changed, we need to trigger a React rerender to update children props. const viewports = deck.getViewports(); if (thisRef.lastRenderedViewports !== viewports) { // Viewports have changed, update children props first. // This will delay the Deck canvas redraw till after React update (in useLayoutEffect) // so that the canvas does not get rendered before the child components update. thisRef.forceUpdate(); } else { redrawDeck(thisRef); } } }); return deck; } function DeckGLWithRef(props, ref) { // A mechanism to force redraw const [version, setVersion] = useState(0); // A reference to persistent states const _thisRef = useRef({ control: null, version, forceUpdate: () => setVersion(v => v + 1) }); const thisRef = _thisRef.current; // DOM refs const containerRef = useRef(null); const canvasRef = useRef(null); // extract any deck.gl layers masquerading as react elements from props.children const jsxProps = useMemo(() => extractJSXLayers(props), [props.layers, props.views, props.children]); // Callbacks let inRender = true; const handleViewStateChange = params => { if (inRender && props.viewState) { // Callback may invoke a state update. Defer callback to after render() to avoid React error // In React StrictMode, render is executed twice and useEffect/useLayoutEffect is executed once // Store deferred parameters in ref so that we can access it in another render thisRef.viewStateUpdateRequested = params; return null; } thisRef.viewStateUpdateRequested = null; return props.onViewStateChange?.(params); }; const handleInteractionStateChange = params => { if (inRender) { // Callback may invoke a state update. Defer callback to after render() to avoid React error // In React StrictMode, render is executed twice and useEffect/useLayoutEffect is executed once // Store deferred parameters in ref so that we can access it in another render thisRef.interactionStateUpdateRequested = params; } else { thisRef.interactionStateUpdateRequested = null; props.onInteractionStateChange?.(params); } }; // 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 = { widgets: [], ...props, // Override user styling props. We will set the canvas style in render() style: null, width: '100%', height: '100%', parent: containerRef.current, canvas: canvasRef.current, layers: jsxProps.layers, views: jsxProps.views, onViewStateChange: handleViewStateChange, onInteractionStateChange: handleInteractionStateChange }; // The defaultValue for _customRender is null, which would overwrite the definition // of _customRender. Remove to avoid frequently redeclaring the method here. delete forwardProps._customRender; if (thisRef.deck) { thisRef.deck.setProps(forwardProps); } return forwardProps; }, [props]); useEffect(() => { const DeckClass = props.Deck || Deck; thisRef.deck = createDeckInstance(thisRef, DeckClass, { ...deckProps, parent: containerRef.current, canvas: canvasRef.current }); return () => thisRef.deck?.finalize(); }, []); 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 // matches the child components. redrawDeck(thisRef); // Execute deferred callbacks const { viewStateUpdateRequested, interactionStateUpdateRequested } = thisRef; if (viewStateUpdateRequested) { handleViewStateChange(viewStateUpdateRequested); } if (interactionStateUpdateRequested) { handleInteractionStateChange(interactionStateUpdateRequested); } }); useImperativeHandle(ref, () => getRefHandles(thisRef), []); const currentViewports = thisRef.deck && thisRef.deck.isInitialized ? thisRef.deck.getViewports() : undefined; const { ContextProvider, width = '100%', height = '100%', id, style } = props; const { containerStyle, canvasStyle } = useMemo(() => extractStyles({ width, height, style }), [width, height, style]); // Props changes may lead to 3 types of updates: // 1. Only the WebGL canvas - updated in Deck's render cycle (next animation frame) // 2. Only the DOM - updated in React's lifecycle (now) // 3. Both the WebGL canvas and the DOM - defer React rerender to next animation frame just // before Deck redraw to ensure perfect synchronization & avoid excessive redraw // This is because multiple changes may happen to Deck between two frames e.g. transition if ((!thisRef.viewStateUpdateRequested && thisRef.lastRenderedViewports === currentViewports) || // case 2 thisRef.version !== version // case 3 just before deck redraws ) { thisRef.lastRenderedViewports = currentViewports; thisRef.version = version; // Render the background elements (typically react-map-gl instances) // using the view descriptors const childrenUnderViews = positionChildrenUnderViews({ children: jsxProps.children, deck: thisRef.deck, ContextProvider }); const canvas = createElement('canvas', { key: 'canvas', id: id || 'deckgl-overlay', ref: canvasRef, style: canvasStyle }); // Render deck.gl as the last child thisRef.control = createElement('div', { id: `${id || 'deckgl'}-wrapper`, ref: containerRef, style: containerStyle }, [canvas, childrenUnderViews]); } inRender = false; return thisRef.control; } const DeckGL = React.forwardRef(DeckGLWithRef); export default DeckGL; //# sourceMappingURL=deckgl.js.map