Skip to content

fix(jupyter): Fix initial viewport sizing and React 18 migration#3253

Open
yharby wants to merge 3 commits into
keplergl:masterfrom
yharby:fix/jupyter-initial-viewport-sizing
Open

fix(jupyter): Fix initial viewport sizing and React 18 migration#3253
yharby wants to merge 3 commits into
keplergl:masterfrom
yharby:fix/jupyter-initial-viewport-sizing

Conversation

@yharby
Copy link
Copy Markdown
Contributor

@yharby yharby commented Dec 3, 2025

Summary

This PR fixes two related issues in the kepler.gl-jupyter HTML export:

  1. Initial viewport sizing - Map doesn't fill the viewport on initial page load, requiring a browser resize to trigger correct sizing
  2. React 18 compatibility - Migrates from deprecated ReactDOM.render to the modern createRoot API

Changes

app.js:

  • Call handleResize() on component mount in the useEffect hook to set correct initial dimensions

root.js:

  • Migrate from ReactDOM.render to React 18's createRoot API
  • Add DataLoader component to handle data loading after Redux Provider mounts
  • This ensures Provider subscriptions are active before dispatching addDataToMap actions

main.js:

  • Remove data loading logic (now handled by DataLoader component in React lifecycle)
  • Simplify to only handle DOM setup and trigger render

Root Cause

The original code had two issues:

  1. handleResize() was only called on the resize event, never on initial mount, so the map container started with 0x0 dimensions until a resize occurred

  2. React 18's createRoot().render() is asynchronous, unlike the synchronous ReactDOM.render(). This caused a race condition where data was dispatched to Redux before the Provider's subscriptions were active, resulting in no layers being displayed

Testing

  • Tested locally with HTML export - map now fills viewport immediately on load
  • Data/config loads correctly with layers displayed
  • No console errors

Fixes #3252

… 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
Copilot AI review requested due to automatic review settings December 3, 2025 15:33
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.render to React 18's createRoot API
  • Moved data loading into React component lifecycle via new DataLoader component 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.

Comment thread bindings/kepler.gl-jupyter/js/lib/keplergl/components/app.js
Comment thread bindings/kepler.gl-jupyter/js/lib/keplergl/components/root.js
- 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
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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});
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

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});
}
Suggested change
setDimension({width, height});
if (
width !== windowDimension.width ||
height !== windowDimension.height
) {
setDimension({width, height});
}

Copilot uses AI. Check for mistakes.
Comment on lines 70 to 75
useEffect(() => {
// Call handleResize on mount to set initial dimensions
handleResize();
window.addEventListener('resize', resizeDelay);
return () => window.removeEventListener('resize', resizeDelay);
}, []);
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

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);
  };
}, []);

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +30
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]);
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

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:

  1. Removing store from the dependency array (if the store reference is guaranteed to be stable)
  2. Resetting hasLoadedData.current = false when the store changes
  3. 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.

Copilot uses AI. Check for mistakes.
@igorDykhta igorDykhta added the jupyter keplergl for Jupyter label Dec 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

jupyter keplergl for Jupyter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

keplergl-jupyter: Map doesn't fill viewport on initial render (requires resize event)

3 participants