diff --git a/bindings/kepler.gl-jupyter/js/lib/keplergl/components/app.js b/bindings/kepler.gl-jupyter/js/lib/keplergl/components/app.js index 281abe195e..9e1f3811a5 100644 --- a/bindings/kepler.gl-jupyter/js/lib/keplergl/components/app.js +++ b/bindings/kepler.gl-jupyter/js/lib/keplergl/components/app.js @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Copyright contributors to the kepler.gl project -import React, {useEffect, useState, useRef} from 'react'; +import {useEffect, useState, useRef} from 'react'; import styled from 'styled-components'; const ReactHelmet = require('react-helmet'); const Helmet = ReactHelmet ? ReactHelmet.Helmet : null; @@ -58,19 +58,18 @@ function App() { const width = rootElm.current.offsetWidth; const height = rootElm.current.offsetHeight; - const dimensionToSet = { - ...(width && width !== windowDimension.width ? {width} : {}), - ...(height && height !== windowDimension.height ? {height} : {}) - }; - - setDimension(dimensionToSet); + if (width && height) { + setDimension({width, height}); + } }; - // in Jupyter Lab, parent component has transition when window resize. + // in Jupyter Lab, parent component has transition when window resize. // need to delay call to get the final parent width, const resizeDelay = () => window.setTimeout(handleResize, 500); useEffect(() => { + // Call handleResize on mount to set initial dimensions + handleResize(); window.addEventListener('resize', resizeDelay); return () => window.removeEventListener('resize', resizeDelay); }, []); diff --git a/bindings/kepler.gl-jupyter/js/lib/keplergl/components/root.js b/bindings/kepler.gl-jupyter/js/lib/keplergl/components/root.js index 03c6f96c44..0046ceabc6 100644 --- a/bindings/kepler.gl-jupyter/js/lib/keplergl/components/root.js +++ b/bindings/kepler.gl-jupyter/js/lib/keplergl/components/root.js @@ -1,19 +1,51 @@ // SPDX-License-Identifier: MIT // Copyright contributors to the kepler.gl project -import React from 'react'; -import ReactDOM from 'react-dom'; +import {useEffect, useRef} from 'react'; +import {createRoot} from 'react-dom/client'; import {Provider} from 'react-redux'; import App from './app'; +import Window from 'global/window'; +import {addDataConfigToKeplerGl} from '../kepler.gl'; -function renderRoot({id, store, ele}) { - const Root = () => ( +// Store root instances to avoid creating multiple roots for the same element +const rootInstances = new WeakMap(); + +// Separate component to handle data loading after mount +function DataLoader({store, onRenderComplete}) { + const hasLoadedData = useRef(false); + + useEffect(() => { + // This runs AFTER component tree is mounted and Provider is fully subscribed + // Load data from Window.__keplerglDataConfig (HTML export mode) + if (!hasLoadedData.current && Window.__keplerglDataConfig) { + hasLoadedData.current = true; + const {data, config, options} = Window.__keplerglDataConfig; + addDataConfigToKeplerGl({data, config, options, store}); + } + // Signal render complete for callback-based loading (Jupyter widget mode) + if (onRenderComplete) { + onRenderComplete(); + } + }, [store, onRenderComplete]); + + return null; +} + +function renderRoot({id, store, ele, onRenderComplete}) { + // Use React 18 createRoot API - reuse existing root if available + let root = rootInstances.get(ele); + if (!root) { + root = createRoot(ele); + rootInstances.set(ele, root); + } + + root.render( + ); - - ReactDOM.render(, ele); } export default renderRoot; diff --git a/bindings/kepler.gl-jupyter/js/lib/keplergl/main.js b/bindings/kepler.gl-jupyter/js/lib/keplergl/main.js index 48abea3076..c3daf294ec 100644 --- a/bindings/kepler.gl-jupyter/js/lib/keplergl/main.js +++ b/bindings/kepler.gl-jupyter/js/lib/keplergl/main.js @@ -5,8 +5,6 @@ import createAppStore from './store'; import renderRoot from './components/root'; import document from 'global/document'; -import Window from 'global/window'; -import {addDataConfigToKeplerGl} from './kepler.gl'; const map = (function initKeplerGl() { const id = 'keplergl-0'; @@ -18,6 +16,8 @@ const map = (function initKeplerGl() { return { render: () => { + // Data loading is now handled inside renderRoot via useEffect + // This ensures Redux Provider subscriptions are active before dispatching renderRoot({id, store, ele: divElmt}); }, store @@ -25,8 +25,3 @@ const map = (function initKeplerGl() { })(); map.render(); - -(function loadDataConfig(keplerGlMap) { - const {data, config, options} = Window.__keplerglDataConfig || {}; - addDataConfigToKeplerGl({data, config, options, store: keplerGlMap.store}); -})(map);