Skip to content

feat(mapbox): Add Controlled Mode and State Callbacks to Widgets #9964

@chrisgervang

Description

@chrisgervang

Summary

Add support for controlled components pattern and state change callbacks across deck.gl widgets. This enables:

  • Parent applications to control widget state via props
  • Notification when widget state changes
  • Better integration with state management libraries (Redux, Zustand, etc.)
  • Consistent API patterns across all widgets

Motivation

Currently, most deck.gl widgets manage their own internal state without providing ways for parent applications to:

  1. Control state externally - For example, syncing theme mode with an application's theme system
  2. Be notified of state changes - For example, knowing when a user toggles fullscreen or changes zoom

Widget Inventory

Widget Internal State Has Callbacks Proposed Changes
ZoomWidget viewports - onZoom
CompassWidget viewports - onCompassReset
GimbalWidget viewports - onGimbalReset
GeocoderWidget addressText, viewports - onGeocode
ResetViewWidget - - onReset
ThemeWidget themeMode - themeMode, onThemeModeChange
FullscreenWidget fullscreen - fullscreen, onFullscreenChange
TimelineWidget currentTime, playing onTimeChange time, playing, onPlayingChange
ViewSelectorWidget viewMode onViewModeChange viewMode
StatsWidget collapsed - collapsed, onCollapsedChange
SplitterWidget split (preact state) onChange, onDragStart, onDragEnd split
LoadingWidget loading (derived) - onLoadingChange
ScreenshotWidget - onCapture -
InfoWidget position, visible, text onClick, getTooltip -
ContextMenuWidget visible, position, menuItems onMenuItemSelected, getMenuItems -
ScaleWidget derived from viewport - -
FpsWidget derived from metrics - -
TooltipWidget isVisible - - (managed by Deck's getTooltip)

Proposed Solution

Controlled/Uncontrolled Pattern

Follow React's controlled component pattern:

// Uncontrolled (current behavior) - widget manages its own state
<ThemeWidget initialThemeMode="dark" />

// Controlled - parent manages state via props
const [themeMode, setThemeMode] = useState('dark');
<ThemeWidget
  themeMode={themeMode}
  onThemeModeChange={setThemeMode}
/>

When the controlled prop is provided (!== undefined), the widget:

  • Uses the prop value as the source of truth
  • Calls the callback on user interaction instead of updating internal state
  • Parent is responsible for updating the prop

Implementation Pattern

_handleClick() {
  const nextMode = this.getThemeMode() === 'dark' ? 'light' : 'dark';

  // Always call callback if provided
  this.props.onThemeModeChange?.(nextMode);

  // Only update internal state if uncontrolled
  if (this.props.themeMode === undefined) {
    this._setThemeMode(nextMode);
  }
}

getThemeMode(): 'light' | 'dark' {
  // Use controlled prop if provided, otherwise internal state
  return this.props.themeMode ?? this.themeMode;
}

Detailed Changes

View State Widgets - Add Callbacks

These widgets modify the view and should notify when changes occur:

ZoomWidget

onZoom?: (params: {
  viewId: string;
  delta: number;  // +1 or -1
  zoom: number;   // new zoom level
}) => void;

CompassWidget

onCompassReset?: (params: {
  viewId: string;
  bearing: number;
  pitch: number;
}) => void;

GimbalWidget

onGimbalReset?: (params: {
  viewId: string;
  rotationOrbit: number;
  rotationX: number;
}) => void;

GeocoderWidget

onGeocode?: (params: {
  viewId: string;
  coordinates: {longitude: number; latitude: number; zoom?: number};
}) => void;

ResetViewWidget

onReset?: (params: {
  viewId: string;
  viewState: ViewState;
}) => void;

Toggle Widgets - Add Controlled Mode

ThemeWidget

/** Controlled theme mode */
themeMode?: 'light' | 'dark';
/** Called when user clicks toggle */
onThemeModeChange?: (mode: 'light' | 'dark') => void;

FullscreenWidget

/** Controlled fullscreen state */
fullscreen?: boolean;
/** Called when fullscreen state changes */
onFullscreenChange?: (fullscreen: boolean) => void;

TimelineWidget (already has onTimeChange)

/** Controlled time value */
time?: number;
/** Controlled playing state */
playing?: boolean;
/** Called when play/pause is toggled */
onPlayingChange?: (playing: boolean) => void;

ViewSelectorWidget (already has onViewModeChange)

/** Controlled view mode */
viewMode?: ViewMode;

StatsWidget

/** Controlled collapsed state */
collapsed?: boolean;
/** Called when collapsed state changes */
onCollapsedChange?: (collapsed: boolean) => void;

SplitterWidget (already has onChange)

/** Controlled split position (0-1) */
split?: number;

Notification-Only Callback

LoadingWidget

/** Called when loading state changes */
onLoadingChange?: (loading: boolean) => void;

Implementation Plan

Phase 1: High-Value Controlled Mode

  1. ThemeWidget - controlled themeMode + onThemeModeChange
  2. FullscreenWidget - controlled fullscreen + onFullscreenChange
  3. TimelineWidget - controlled time, playing + onPlayingChange

Phase 2: View State Callbacks

  1. ZoomWidget - onZoom
  2. CompassWidget - onCompassReset
  3. GimbalWidget - onGimbalReset
  4. GeocoderWidget - onGeocode
  5. ResetViewWidget - onReset

Phase 3: Remaining Widgets

  1. ViewSelectorWidget - controlled viewMode
  2. SplitterWidget - controlled split
  3. StatsWidget - controlled collapsed + onCollapsedChange
  4. LoadingWidget - onLoadingChange

Breaking Changes

None. All changes are additive:

  • New optional props for controlled mode
  • New optional callback props
  • Existing uncontrolled behavior preserved as default

Checklist

  • ThemeWidget: Add controlled themeMode + onThemeModeChange
  • FullscreenWidget: Add controlled fullscreen + onFullscreenChange
  • TimelineWidget: Add controlled time, playing + onPlayingChange
  • ZoomWidget: Add onZoom callback
  • CompassWidget: Add onCompassReset callback
  • GimbalWidget: Add onGimbalReset callback
  • GeocoderWidget: Add onGeocode callback
  • ResetViewWidget: Add onReset callback
  • ViewSelectorWidget: Add controlled viewMode
  • SplitterWidget: Add controlled split
  • StatsWidget: Add controlled collapsed + onCollapsedChange
  • LoadingWidget: Add onLoadingChange notification
  • Update documentation
  • Add examples showing controlled usage
  • Update React widget wrappers to pass through new props

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions