fix(jupyter): Fix initial viewport sizing and React 18 migration#3253
fix(jupyter): Fix initial viewport sizing and React 18 migration#3253yharby wants to merge 3 commits into
Conversation
… sizing The Kepler.gl map doesn't fill the viewport on initial render because the useEffect in app.js adds a resize listener but never calls handleResize() on mount. This causes the component to use default dimensions (800x400) until a resize event occurs. Changes: - app.js: Call handleResize() on initial mount in useEffect - root.js: Upgrade to React 18 createRoot API (replaces deprecated ReactDOM.render) Fixes keplergl#3252
- Update root.js to use React 18 createRoot API instead of deprecated ReactDOM.render - Add DataLoader component to handle data loading after Provider mount - Move data loading from main.js into React component lifecycle - This ensures Redux Provider subscriptions are active before dispatching actions - Fixes data/config not loading in HTML export mode
There was a problem hiding this comment.
Pull request overview
This PR addresses viewport sizing issues in kepler.gl-jupyter HTML exports and migrates to React 18's modern rendering API. The changes fix a bug where maps didn't render at the correct size on initial load and resolve a race condition introduced by React 18's asynchronous rendering.
Key changes:
- Added initial resize handler call on component mount to set correct viewport dimensions
- Migrated from deprecated
ReactDOM.renderto React 18'screateRootAPI - Moved data loading into React component lifecycle via new
DataLoadercomponent to ensure Redux Provider subscriptions are active before dispatching actions
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| bindings/kepler.gl-jupyter/js/lib/keplergl/main.js | Removed data loading logic (now handled in root.js) and cleaned up unused imports |
| bindings/kepler.gl-jupyter/js/lib/keplergl/components/root.js | Migrated to React 18 createRoot API and added DataLoader component to handle data loading after Redux Provider mounts |
| bindings/kepler.gl-jupyter/js/lib/keplergl/components/app.js | Added handleResize() call on component mount to fix initial viewport sizing |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Simplify handleResize to directly set dimensions without stale state comparison - Add WeakMap to store and reuse createRoot instances, preventing memory leaks - Keep minimal change approach for viewport sizing fix
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| setDimension(dimensionToSet); | ||
| if (width && height) { | ||
| setDimension({width, height}); |
There was a problem hiding this comment.
The dimension update logic has been simplified, but this may cause unnecessary re-renders. The previous implementation only updated state when dimensions actually changed:
const dimensionToSet = {
...(width && width !== windowDimension.width ? {width} : {}),
...(height && height !== windowDimension.height ? {height} : {})
};
setDimension(dimensionToSet);The new logic always calls setDimension({width, height}) even if the dimensions haven't changed. Consider adding a check to only update when dimensions actually change:
if ((width && width !== windowDimension.width) || (height && height !== windowDimension.height)) {
setDimension({width, height});
}| setDimension({width, height}); | |
| if ( | |
| width !== windowDimension.width || | |
| height !== windowDimension.height | |
| ) { | |
| setDimension({width, height}); | |
| } |
| useEffect(() => { | ||
| // Call handleResize on mount to set initial dimensions | ||
| handleResize(); | ||
| window.addEventListener('resize', resizeDelay); | ||
| return () => window.removeEventListener('resize', resizeDelay); | ||
| }, []); |
There was a problem hiding this comment.
The cleanup function in useEffect doesn't clear the timeout created by resizeDelay(). If the component unmounts while a timeout is pending, the timeout callback will still execute, potentially calling handleResize() on an unmounted component.
Consider storing the timeout ID and clearing it in the cleanup:
useEffect(() => {
handleResize();
let timeoutId = null;
const resizeHandler = () => {
if (timeoutId) clearTimeout(timeoutId);
timeoutId = window.setTimeout(handleResize, 500);
};
window.addEventListener('resize', resizeHandler);
return () => {
window.removeEventListener('resize', resizeHandler);
if (timeoutId) clearTimeout(timeoutId);
};
}, []);| 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]); |
There was a problem hiding this comment.
Including store in the useEffect dependency array may cause unexpected behavior. If the store reference changes (which shouldn't happen in normal usage), the effect will re-run but hasLoadedData.current will still be true, preventing data from being reloaded with the new store.
Consider either:
- Removing
storefrom the dependency array (if the store reference is guaranteed to be stable) - Resetting
hasLoadedData.current = falsewhen the store changes - Using a WeakMap to track loaded state per store instance
The current implementation assumes the store reference never changes while also including it in dependencies, which is inconsistent.
Summary
This PR fixes two related issues in the kepler.gl-jupyter HTML export:
ReactDOM.renderto the moderncreateRootAPIChanges
app.js:
handleResize()on component mount in theuseEffecthook to set correct initial dimensionsroot.js:
ReactDOM.renderto React 18'screateRootAPIDataLoadercomponent to handle data loading after Redux Provider mountsaddDataToMapactionsmain.js:
Root Cause
The original code had two issues:
handleResize()was only called on the resize event, never on initial mount, so the map container started with 0x0 dimensions until a resize occurredReact 18's
createRoot().render()is asynchronous, unlike the synchronousReactDOM.render(). This caused a race condition where data was dispatched to Redux before the Provider's subscriptions were active, resulting in no layers being displayedTesting
Fixes #3252