Skip to content

Bind widgets to query params - Part 2#13682

Merged
mayagbarnes merged 15 commits intodevelopfrom
query-param-bind-2
Feb 4, 2026
Merged

Bind widgets to query params - Part 2#13682
mayagbarnes merged 15 commits intodevelopfrom
query-param-bind-2

Conversation

@mayagbarnes
Copy link
Copy Markdown
Collaborator

@mayagbarnes mayagbarnes commented Jan 23, 2026

Describe your changes

Bind Widgets to Query Params - Part 2
This PR adds the frontend infrastructure for binding widgets to query parameters - enabling two-way sync between widget interactions and the browser's URL.

  • Binding registration - registerQueryParamBinding() / unregisterQueryParamBinding() to track widget-to-param relationships on the frontend
  • URL sync methods - syncToUrlIfBound(), syncCommaArrayToUrlIfBound(), syncRepeatedArrayToUrlIfBound() update the URL when widgets change
  • Default value detection - Clears URL param when widget returns to default (keeps URLs clean)
  • Index-to-string mapping - Converts selection widget indices to human-readable option strings in URLs
  • App integration - handleQueryParamsFromWidget() keeps App state in sync with URL changes
  • MPA page navigation - filterParamsForPageChange() preserves embed params and bound widget params while clearing other query params (like ?foo=bar) when navigating between pages - page-specific ones are cleared by the backend
  • setValue handling in useBasicWidgetState - Uses setValue flag to correctly initialize widget state from backend (URL-seeded or session_state values), with WidgetStateManager persisting values across React Strict Mode remounts

Testing Plan

  • Javascript Unit Tests: ✅ Added
  • Manual Testing: Via widgets in downstream PRs & with prototype

@snyk-io
Copy link
Copy Markdown
Contributor

snyk-io bot commented Jan 23, 2026

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Jan 23, 2026

✅ PR preview is ready!

Name Link
📦 Wheel file https://core-previews.s3-us-west-2.amazonaws.com/pr-13682/streamlit-1.53.1-py3-none-any.whl
📦 @streamlit/component-v2-lib Download from artifacts
🕹️ Preview app pr-13682.streamlit.app (☁️ Deploy here if not accessible)

@mayagbarnes mayagbarnes changed the title Translate prototype code Bind widgets to query params - Part 2 Jan 23, 2026
Copy link
Copy Markdown
Collaborator Author

mayagbarnes commented Jan 23, 2026

@mayagbarnes mayagbarnes added change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users security-assessment-completed labels Jan 23, 2026 — with Graphite App
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Jan 23, 2026

📉 Python coverage change detected

The Python unit test coverage has decreased by 0.0000%

  • Current PR: 93.1073% (23329 statements, 1608 missed)
  • Latest develop: 93.1073% (23329 statements, 1608 missed)

✅ Coverage change is within normal range.

Coverage by files
Name Stmts Miss Cover
streamlit/__init__.py 136 0 100%
streamlit/__main__.py 3 3 0%
streamlit/auth_util.py 231 25 89%
streamlit/cli_util.py 39 6 85%
streamlit/column_config.py 3 0 100%
streamlit/commands/__init__.py 0 0 100%
streamlit/commands/echo.py 54 2 96%
streamlit/commands/execution_control.py 70 10 86%
streamlit/commands/logo.py 53 1 98%
streamlit/commands/navigation.py 106 2 98%
streamlit/commands/page_config.py 106 4 96%
streamlit/components/__init__.py 0 0 100%
streamlit/components/lib/__init__.py 0 0 100%
streamlit/components/lib/local_component_registry.py 35 2 94%
streamlit/components/types/__init__.py 0 0 100%
streamlit/components/types/base_component_registry.py 14 0 100%
streamlit/components/types/base_custom_component.py 48 6 88%
streamlit/components/v1/__init__.py 5 0 100%
streamlit/components/v1/component_arrow.py 33 2 94%
streamlit/components/v1/component_registry.py 41 3 93%
streamlit/components/v1/components.py 4 4 0%
streamlit/components/v1/custom_component.py 84 7 92%
streamlit/components/v2/__init__.py 27 0 100%
streamlit/components/v2/bidi_component/__init__.py 4 0 100%
streamlit/components/v2/bidi_component/constants.py 5 0 100%
streamlit/components/v2/bidi_component/main.py 148 17 89%
streamlit/components/v2/bidi_component/serialization.py 81 2 98%
streamlit/components/v2/bidi_component/state.py 13 0 100%
streamlit/components/v2/component_definition_resolver.py 30 0 100%
streamlit/components/v2/component_file_watcher.py 117 9 92%
streamlit/components/v2/component_manager.py 97 13 87%
streamlit/components/v2/component_manifest_handler.py 24 0 100%
streamlit/components/v2/component_path_utils.py 65 5 92%
streamlit/components/v2/component_registry.py 121 8 93%
streamlit/components/v2/get_bidi_component_manager.py 8 1 88%
streamlit/components/v2/manifest_scanner.py 227 25 89%
streamlit/components/v2/presentation.py 84 19 77%
streamlit/components/v2/types.py 8 8 0%
streamlit/config.py 417 12 97%
streamlit/config_option.py 79 3 96%
streamlit/config_util.py 308 7 98%
streamlit/connections/__init__.py 6 0 100%
streamlit/connections/base_connection.py 49 0 100%
streamlit/connections/snowflake_connection.py 98 16 84%
streamlit/connections/snowpark_connection.py 44 3 93%
streamlit/connections/sql_connection.py 56 6 89%
streamlit/connections/util.py 33 0 100%
streamlit/cursor.py 130 1 99%
streamlit/dataframe_util.py 506 47 91%
streamlit/delta_generator.py 251 7 97%
streamlit/delta_generator_singletons.py 74 7 91%
streamlit/deprecation_util.py 66 4 94%
streamlit/development.py 1 0 100%
streamlit/elements/__init__.py 0 0 100%
streamlit/elements/alert.py 60 0 100%
streamlit/elements/arrow.py 203 15 93%
streamlit/elements/balloons.py 10 0 100%
streamlit/elements/bokeh_chart.py 9 0 100%
streamlit/elements/code.py 20 1 95%
streamlit/elements/deck_gl_json_chart.py 104 7 93%
streamlit/elements/dialog_decorator.py 38 0 100%
streamlit/elements/doc_string.py 227 9 96%
streamlit/elements/empty.py 16 4 75%
streamlit/elements/exception.py 101 10 90%
streamlit/elements/form.py 56 2 96%
streamlit/elements/graphviz_chart.py 36 1 97%
streamlit/elements/heading.py 56 0 100%
streamlit/elements/html.py 49 0 100%
streamlit/elements/iframe.py 29 0 100%
streamlit/elements/image.py 32 0 100%
streamlit/elements/json.py 48 6 88%
streamlit/elements/layouts.py 140 3 98%
streamlit/elements/lib/__init__.py 0 0 100%
streamlit/elements/lib/built_in_chart_utils.py 403 30 93%
streamlit/elements/lib/color_util.py 103 4 96%
streamlit/elements/lib/column_config_utils.py 169 1 99%
streamlit/elements/lib/column_types.py 190 4 98%
streamlit/elements/lib/dialog.py 69 1 99%
streamlit/elements/lib/dicttools.py 39 2 95%
streamlit/elements/lib/file_uploader_utils.py 30 0 100%
streamlit/elements/lib/form_utils.py 26 0 100%
streamlit/elements/lib/image_utils.py 176 21 88%
streamlit/elements/lib/js_number.py 28 3 89%
streamlit/elements/lib/layout_utils.py 121 1 99%
streamlit/elements/lib/mutable_status_container.py 73 4 95%
streamlit/elements/lib/options_selector_utils.py 142 2 99%
streamlit/elements/lib/pandas_styler_utils.py 80 2 98%
streamlit/elements/lib/policies.py 56 1 98%
streamlit/elements/lib/shortcut_utils.py 42 2 95%
streamlit/elements/lib/streamlit_plotly_theme.py 48 0 100%
streamlit/elements/lib/subtitle_utils.py 76 5 93%
streamlit/elements/lib/utils.py 76 5 93%
streamlit/elements/map.py 110 1 99%
streamlit/elements/markdown.py 64 2 97%
streamlit/elements/media.py 181 8 96%
streamlit/elements/metric.py 104 0 100%
streamlit/elements/pdf.py 49 2 96%
streamlit/elements/plotly_chart.py 129 6 95%
streamlit/elements/progress.py 36 0 100%
streamlit/elements/pyplot.py 39 2 95%
streamlit/elements/snow.py 10 0 100%
streamlit/elements/space.py 12 0 100%
streamlit/elements/spinner.py 44 3 93%
streamlit/elements/text.py 16 0 100%
streamlit/elements/toast.py 26 0 100%
streamlit/elements/vega_charts.py 238 5 98%
streamlit/elements/widgets/__init__.py 0 0 100%
streamlit/elements/widgets/audio_input.py 68 1 99%
streamlit/elements/widgets/button.py 245 6 98%
streamlit/elements/widgets/button_group.py 128 0 100%
streamlit/elements/widgets/camera_input.py 62 1 98%
streamlit/elements/widgets/chat.py 237 38 84%
streamlit/elements/widgets/checkbox.py 52 0 100%
streamlit/elements/widgets/color_picker.py 59 2 97%
streamlit/elements/widgets/data_editor.py 254 14 94%
streamlit/elements/widgets/feedback.py 69 0 100%
streamlit/elements/widgets/file_uploader.py 108 10 91%
streamlit/elements/widgets/multiselect.py 114 5 96%
streamlit/elements/widgets/number_input.py 146 4 97%
streamlit/elements/widgets/radio.py 103 5 95%
streamlit/elements/widgets/select_slider.py 122 2 98%
streamlit/elements/widgets/selectbox.py 97 3 97%
streamlit/elements/widgets/slider.py 241 8 97%
streamlit/elements/widgets/text_widgets.py 130 6 95%
streamlit/elements/widgets/time_widgets.py 425 21 95%
streamlit/elements/write.py 166 20 88%
streamlit/emojis.py 4 0 100%
streamlit/env_util.py 21 3 86%
streamlit/error_util.py 33 2 94%
streamlit/errors.py 184 25 86%
streamlit/external/__init__.py 0 0 100%
streamlit/external/langchain/__init__.py 2 0 100%
streamlit/external/langchain/streamlit_callback_handler.py 141 82 42%
streamlit/file_util.py 84 8 90%
streamlit/git_util.py 100 5 95%
streamlit/logger.py 54 0 100%
streamlit/material_icon_names.py 1 0 100%
streamlit/navigation/__init__.py 0 0 100%
streamlit/navigation/page.py 78 2 97%
streamlit/net_util.py 55 3 95%
streamlit/path_security.py 17 1 94%
streamlit/platform.py 10 1 90%
streamlit/runtime/__init__.py 8 0 100%
streamlit/runtime/app_session.py 469 85 82%
streamlit/runtime/caching/__init__.py 21 0 100%
streamlit/runtime/caching/cache_data_api.py 191 3 98%
streamlit/runtime/caching/cache_errors.py 44 4 91%
streamlit/runtime/caching/cache_resource_api.py 165 1 99%
streamlit/runtime/caching/cache_type.py 11 1 91%
streamlit/runtime/caching/cache_utils.py 176 9 95%
streamlit/runtime/caching/cached_message_replay.py 108 1 99%
streamlit/runtime/caching/hashing.py 310 25 92%
streamlit/runtime/caching/legacy_cache_api.py 14 0 100%
streamlit/runtime/caching/storage/__init__.py 2 0 100%
streamlit/runtime/caching/storage/cache_storage_protocol.py 29 0 100%
streamlit/runtime/caching/storage/dummy_cache_storage.py 21 0 100%
streamlit/runtime/caching/storage/in_memory_cache_storage_wrapper.py 67 1 99%
streamlit/runtime/caching/storage/local_disk_cache_storage.py 86 4 95%
streamlit/runtime/caching/ttl_cleanup_cache.py 28 0 100%
streamlit/runtime/connection_factory.py 96 11 89%
streamlit/runtime/context.py 137 0 100%
streamlit/runtime/context_util.py 18 0 100%
streamlit/runtime/credentials.py 139 4 97%
streamlit/runtime/download_data_util.py 27 0 100%
streamlit/runtime/forward_msg_cache.py 23 2 91%
streamlit/runtime/forward_msg_queue.py 63 4 94%
streamlit/runtime/fragment.py 112 2 98%
streamlit/runtime/media_file_manager.py 110 7 94%
streamlit/runtime/media_file_storage.py 15 0 100%
streamlit/runtime/memory_media_file_storage.py 73 0 100%
streamlit/runtime/memory_session_storage.py 15 0 100%
streamlit/runtime/memory_uploaded_file_manager.py 46 1 98%
streamlit/runtime/metrics_util.py 195 13 93%
streamlit/runtime/pages_manager.py 59 2 97%
streamlit/runtime/runtime.py 252 16 94%
streamlit/runtime/runtime_util.py 30 1 97%
streamlit/runtime/script_data.py 16 0 100%
streamlit/runtime/scriptrunner/__init__.py 5 0 100%
streamlit/runtime/scriptrunner/exec_code.py 49 5 90%
streamlit/runtime/scriptrunner/magic.py 83 1 99%
streamlit/runtime/scriptrunner/magic_funcs.py 10 1 90%
streamlit/runtime/scriptrunner/script_cache.py 27 0 100%
streamlit/runtime/scriptrunner/script_runner.py 235 27 89%
streamlit/runtime/scriptrunner_utils/__init__.py 0 0 100%
streamlit/runtime/scriptrunner_utils/exceptions.py 9 1 89%
streamlit/runtime/scriptrunner_utils/script_requests.py 106 5 95%
streamlit/runtime/scriptrunner_utils/script_run_context.py 118 0 100%
streamlit/runtime/secrets.py 241 25 90%
streamlit/runtime/session_manager.py 71 2 97%
streamlit/runtime/state/__init__.py 7 0 100%
streamlit/runtime/state/common.py 55 1 98%
streamlit/runtime/state/presentation.py 19 4 79%
streamlit/runtime/state/query_params.py 274 6 98%
streamlit/runtime/state/query_params_proxy.py 71 0 100%
streamlit/runtime/state/safe_session_state.py 77 9 88%
streamlit/runtime/state/session_state.py 503 36 93%
streamlit/runtime/state/session_state_proxy.py 62 8 87%
streamlit/runtime/state/widgets.py 19 0 100%
streamlit/runtime/stats.py 132 4 97%
streamlit/runtime/theme_util.py 46 1 98%
streamlit/runtime/uploaded_file_manager.py 39 3 92%
streamlit/runtime/websocket_session_manager.py 116 0 100%
streamlit/source_util.py 36 1 97%
streamlit/starlette.py 2 0 100%
streamlit/string_util.py 93 9 90%
streamlit/temporary_directory.py 18 1 94%
streamlit/testing/__init__.py 0 0 100%
streamlit/testing/v1/__init__.py 2 0 100%
streamlit/testing/v1/app_test.py 245 5 98%
streamlit/testing/v1/element_tree.py 1409 85 94%
streamlit/testing/v1/local_script_runner.py 71 2 97%
streamlit/testing/v1/util.py 17 0 100%
streamlit/time_util.py 28 1 96%
streamlit/type_util.py 148 16 89%
streamlit/url_util.py 39 4 90%
streamlit/user_info.py 105 8 92%
streamlit/util.py 38 1 97%
streamlit/version.py 3 0 100%
streamlit/watcher/__init__.py 3 0 100%
streamlit/watcher/event_based_path_watcher.py 184 25 86%
streamlit/watcher/folder_black_list.py 14 1 93%
streamlit/watcher/local_sources_watcher.py 127 9 93%
streamlit/watcher/path_watcher.py 42 3 93%
streamlit/watcher/polling_path_watcher.py 55 2 96%
streamlit/watcher/util.py 59 1 98%
streamlit/web/__init__.py 0 0 100%
streamlit/web/bootstrap.py 174 21 88%
streamlit/web/cache_storage_manager_config.py 5 0 100%
streamlit/web/cli.py 188 16 91%
streamlit/web/server/__init__.py 5 0 100%
streamlit/web/server/app_discovery.py 104 5 95%
streamlit/web/server/app_static_file_handler.py 35 3 91%
streamlit/web/server/authlib_tornado_integration.py 42 5 88%
streamlit/web/server/bidi_component_request_handler.py 65 8 88%
streamlit/web/server/browser_websocket_handler.py 147 20 86%
streamlit/web/server/component_file_utils.py 27 0 100%
streamlit/web/server/component_request_handler.py 55 4 93%
streamlit/web/server/media_file_handler.py 65 9 86%
streamlit/web/server/oauth_authlib_routes.py 162 35 78%
streamlit/web/server/oidc_mixin.py 46 0 100%
streamlit/web/server/routes.py 90 7 92%
streamlit/web/server/server.py 195 13 93%
streamlit/web/server/server_util.py 72 5 93%
streamlit/web/server/starlette/__init__.py 3 0 100%
streamlit/web/server/starlette/starlette_app.py 148 4 97%
streamlit/web/server/starlette/starlette_app_utils.py 101 7 93%
streamlit/web/server/starlette/starlette_auth_routes.py 233 51 78%
streamlit/web/server/starlette/starlette_gzip_middleware.py 30 0 100%
streamlit/web/server/starlette/starlette_path_security_middleware.py 22 0 100%
streamlit/web/server/starlette/starlette_routes.py 346 86 75%
streamlit/web/server/starlette/starlette_server.py 167 7 96%
streamlit/web/server/starlette/starlette_server_config.py 13 0 100%
streamlit/web/server/starlette/starlette_static_routes.py 70 6 91%
streamlit/web/server/starlette/starlette_websocket.py 203 23 89%
streamlit/web/server/stats_request_handler.py 59 5 92%
streamlit/web/server/upload_file_request_handler.py 59 7 88%
streamlit/web/server/websocket_headers.py 19 1 95%
TOTAL 23329 1608 93%

📊 View detailed coverage comparison

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Jan 23, 2026

📈 Frontend coverage change detected

The frontend unit test (vitest) coverage has increased by 0.0100%

  • Current PR: 86.4500% (13841 lines, 1875 missed)
  • Latest develop: 86.4400% (13715 lines, 1859 missed)

✅ Coverage change is within normal range.

📊 View detailed coverage comparison

@sfc-gh-mbarnes sfc-gh-mbarnes force-pushed the query-param-bind-2 branch 2 times, most recently from a3f6a97 to 58e746c Compare January 23, 2026 21:39
@mayagbarnes mayagbarnes changed the base branch from query-param-bind-1 to graphite-base/13682 January 23, 2026 22:28
@mayagbarnes mayagbarnes marked this pull request as ready for review January 23, 2026 22:28
@mayagbarnes mayagbarnes requested a review from Copilot January 23, 2026 22:36
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

Adds frontend infrastructure for binding widget values to URL query parameters, enabling two-way sync between widget state and the browser URL (including preservation behavior across multipage navigation).

Changes:

  • Introduces query param binding registration and URL-sync logic in WidgetStateManager (including default-value clearing and option index→string mapping).
  • Integrates widget-driven query param updates into App state to preserve relevant params on page navigation.
  • Updates useBasicWidgetState initialization semantics around setValue, and adds/updates unit tests.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
frontend/lib/src/hooks/useBasicWidgetState.ts Initializes widget state from proto value when backend sets setValue.
frontend/lib/src/hooks/useBasicWidgetState.test.ts Adds unit tests for setValue/default initialization behavior.
frontend/lib/src/components/widgets/ButtonGroup/ButtonGroup.test.tsx Updates expectation to initialize from .value when setValue is true.
frontend/lib/src/WidgetStateManager.ts Adds query param binding registry, URL sync helpers, and navigation param filtering.
frontend/lib/src/WidgetStateManager.test.ts Adds unit tests for query param binding behavior and URL sync.
frontend/app/src/App.tsx Wires widget URL updates into App state and preserves bound params on page changes.
frontend/app/src/App.test.tsx Updates tests to reflect new query param preservation/clearing behavior on navigation.

@mayagbarnes mayagbarnes changed the base branch from graphite-base/13682 to query-param-bind-1 January 23, 2026 23:05
@lukasmasuch
Copy link
Copy Markdown
Collaborator

@cursor review

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

@github-actions
Copy link
Copy Markdown
Contributor

Summary

Adds query param binding support in the frontend by syncing bound widget updates to the URL, preserving bound params across page navigation, and initializing App state from the current URL. Updates useBasicWidgetState to honor setValue for initial state and adjusts unit tests accordingly.

Code Quality

Encapsulation is clean: query-param logic lives in WidgetStateManager with small, testable helpers, and App only wires state + host messaging. The changes are readable and consistent with existing patterns.

Test Coverage

Strong unit coverage was added for query param binding and useBasicWidgetState, and App navigation tests were updated. No e2e tests were added; if this feature is user-facing, an e2e scenario would provide additional confidence.

Backwards Compatibility

Behavior should remain compatible provided the backend correctly sets setValue when seeding widget values. The new useBasicWidgetState logic relies on setValue rather than value != default.

Security & Risk

No new security concerns identified. URL updates are local and use URLSearchParams, reducing injection risk. Overall regression risk appears low.

Recommendations

  1. Consider adding a focused e2e test that covers a bound widget and verifies query param preservation across page navigation (including embed params).

Verdict

APPROVED: Solid implementation with thorough unit tests; only optional e2e coverage is missing.


This is an automated AI review using gpt-5.2-codex-high. Please verify the feedback and use your judgment.

Base automatically changed from query-param-bind-1 to develop January 26, 2026 20:22
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Jan 26, 2026

📈 Significant bundle size change detected

Metric This Branch develop Change (%)
Total (gzip) 8.05 MiB 8.05 MiB +0.02%
Entry (gzip) 717.19 KiB 712.91 KiB +0.60%

Please verify that this change is expected.

📊 View detailed bundle comparison

@mayagbarnes mayagbarnes added the ai-review If applied to PR or issue will run AI review workflow label Feb 2, 2026
@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Feb 2, 2026
@mayagbarnes mayagbarnes added the ai-review If applied to PR or issue will run AI review workflow label Feb 3, 2026
@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Feb 3, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 3, 2026

Summary

This PR adds frontend infrastructure for binding widgets to URL query parameters, enabling two-way synchronization between widget interactions and the browser's URL. Key changes include:

  1. WidgetStateManager enhancements (WidgetStateManager.ts):

    • New binding registry (boundWidgets, paramKeyToWidgetId) for tracking widget-to-param relationships
    • URL sync methods (registerQueryParamBinding, filterParamsForPageChange, syncToUrlIfBound, etc.)
    • Default value detection to keep URLs clean when widgets return to default state
    • Index-to-string mapping for selection widgets
  2. App integration (App.tsx):

    • Initialize queryParams from URL to preserve bound widget params from shared links
    • New handleQueryParamsFromWidget callback for URL change notifications
    • Modified page navigation to use filterParamsForPageChange for preserving bound widget params
  3. Widget state hook improvements (useBasicWidgetState.ts):

    • Uses setValue flag to correctly initialize widget state from backend (URL-seeded or session_state values)
    • Relies on WidgetStateManager to persist values across React Strict Mode remounts

Code Quality

Strengths:

  • Clean, well-structured implementation that follows existing codebase patterns
  • Good separation of concerns between pure conversion logic and side-effect URL updates in WidgetStateManager
  • Clear comments explaining the purpose of new methods
  • Proper TypeScript types for WidgetValueType, QueryParamBinding

Code structure observations:

  const getDefaultState = useCallback<(wm: WidgetStateManager, el: P) => T>(
    (_wm, el) => {
      // Backend explicitly set a value (e.g., from URL params or session_state).
      // This handles both initial URL seeding and session_state updates.
      // On React Strict Mode remount, WidgetStateManager will have the value
      // (stored by the first mount's effect), so this path won't be reached.
      if (el.setValue) {
        return getCurrStateFromProto(el)
      }

      return getDefaultStateFromProto(el)
    },
    [getDefaultStateFromProto, getCurrStateFromProto]
  )

Good pattern using setValue as an explicit signal from the backend rather than inferring seeding from value !== default.

  private maybeSyncValueToUrl(
    widgetId: string,
    source: Source,
    value: unknown
  ): void {
    if (!source.fromUi) return

    const binding = this.boundWidgets.get(widgetId)
    if (!binding) return

    // Pure: Convert value to URL format
    const urlValue = this.convertToUrlValue(value, binding)

    // Pure: Check if we should clear the param
    const shouldClear = this.shouldClearUrlParam(urlValue, binding)

    // Side effect: Update URL
    this.updateUrlParam(
      binding.paramKey,
      shouldClear ? null : urlValue,
      binding.urlFormat
    )
  }

Clean separation of pure logic and side effects.

Minor notes:

  • Multiple TODO comments about removing options after wire format changes (e.g., lines 73-75, 1205, 1219) - ensure these are tracked for cleanup in future PRs

Test Coverage

Excellent test coverage with 524+ new lines of tests for query param binding. The tests follow best practices:

  • WidgetStateManager.test.ts: Comprehensive tests covering:

    • Registration/unregistration of bindings
    • URL sync for all scalar value types (bool, int, double, string)
    • URL sync for array values with different formats (repeated, comma)
    • Index-to-option string conversion for selection widgets
    • Default value detection and URL param clearing
    • Edge cases (null values, invalid values, no handler set)
    • filterParamsForPageChange preservation logic
  • useBasicWidgetState.test.ts (new file): Tests the setValue behavior:

    • Uses currValue when setValue is true
    • Uses defaultValue when setValue is false
    • WidgetStateManager value takes precedence when available
    • Tests for arrays and numeric values
  • App.test.tsx: Updated tests correctly verify:

    • Query params preservation during page navigation
    • Proper filtering and clearing of non-bound params
    • Uses Mock type from Vitest appropriately

Test pattern quality:

  • Proper beforeEach/afterEach for setup/teardown
  • Correct mocking of window.location and window.history.replaceState
  • Good use of it.each for parameterized tests
  • Includes both positive assertions and negative/anti-regression checks

Backwards Compatibility

The changes are backwards compatible:

  • All new functionality is additive (new methods, new state fields)
  • Existing widgets without query param bindings continue to work unchanged
  • The setValue flag behavior in useBasicWidgetState is consistent with existing session_state update patterns
  • No breaking changes to existing APIs

The URL initialization change in App.tsx:

queryParams: window.location?.search?.replace(/^\?/, "") ?? "",

Is safe as it only affects the initial state and preserves expected behavior for shared links.

Security & Risk

No security concerns identified:

  • URL manipulation uses the standard window.history.replaceState API appropriately
  • The query-string library is used correctly for URL encoding/decoding
  • No user input is directly executed or stored unsafely
  • Bound widget values come from the trusted backend

Low regression risk:

  • Changes are well-isolated to specific code paths
  • Unbound widgets are not affected by the new sync logic
  • The fromUi check ensures only user-initiated changes sync to URL

Potential edge case to monitor:

  • Rapid page navigation could theoretically read stale URL params in filterParamsForPageChange, but this is unlikely in practice and would self-correct on the next navigation.

Recommendations

  1. Track TODO cleanup: The TODO comments about removing options normalization after wire format changes should be tracked in an issue to ensure they're addressed in a future PR.

  2. E2E test consideration: The PR description mentions testing "via widgets in downstream PRs & with prototype." Consider adding an E2E test in the final PR of this feature to verify the complete URL-to-widget synchronization flow works end-to-end.

  3. Documentation: Once the feature is complete across all PRs, consider adding developer documentation explaining the query param binding architecture for future maintainers.

Verdict

APPROVED: This PR provides well-structured, thoroughly tested frontend infrastructure for widget-to-URL query param binding. The code follows existing patterns, has comprehensive test coverage (500+ new lines), is backwards compatible, and introduces no security concerns. The implementation is clean and ready for merge.


This is an automated AI review using opus-4.5-thinking. Please verify the feedback and use your judgment.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 3, 2026

Summary

This PR adds frontend infrastructure to bind widget values to URL query parameters (two-way-ish sync for UI → URL, plus preserving bound params during page navigation). The core work is in frontend/lib/src/WidgetStateManager.ts (binding registry + URL update helpers) and frontend/app/src/App.tsx (keeping App state and host communication in sync).

Code Quality

  • Overall structure: The new API on WidgetStateManager is cohesive and reasonably separated into “pure” conversion logic vs URL side effects (convertToUrlValue / shouldClearUrlParam vs updateUrlParam / maybeSyncValueToUrl). The App integration (setQueryParamsChangeHandler) is minimal and keeps navigation logic centralized.

  • Potential regression in setDoubleArrayValue early-return: In frontend/lib/src/WidgetStateManager.ts the new “filter invalid values” behavior returns early when all values are invalid:

    • setDoubleArrayValue filters and then returns without calling createWidgetState or onWidgetValueChanged when validValue.length === 0 (L607-L615).
    • This is risky because it changes semantics for any call path that can produce an “all invalid” array (including backend-originated updates where source.fromUi is false). Even though maybeSyncValueToUrl will no-op for fromUi: false, the function still returns early and skips updating WidgetStateManager state and skips triggering a rerun/update flush.
    • File reference: frontend/lib/src/WidgetStateManager.ts L607-L620.
  • Duplicate paramKey handling: registerQueryParamBinding overwrites paramKeyToWidgetId for the paramKey but does not reconcile a previously-bound widget that used the same paramKey (the old widget remains in boundWidgets). If the backend ever allows duplicate keys (or transient duplication during remounts), this can lead to inconsistent state.

    • File reference: frontend/lib/src/WidgetStateManager.ts L1102-L1110.
  • filterParamsForPageChange reads from window.location: This is a pragmatic choice, but it means navigation preservation is tied to the current URL rather than App state. That’s likely intended, but it’s worth noting as it can be surprising if URL state and App state diverge.

    • File reference: frontend/lib/src/WidgetStateManager.ts L1138-L1168 and frontend/app/src/App.tsx L1902-L1908.

Test Coverage

  • Frontend unit tests: Good coverage was added for query-param binding mechanics in frontend/lib/src/WidgetStateManager.test.ts (scalar types, arrays with repeated vs comma format, default clearing, and fromUi gating). A new unit test file validates the setValue semantics in useBasicWidgetState (frontend/lib/src/hooks/useBasicWidgetState.test.ts).

  • App-level tests: frontend/app/src/App.test.tsx was updated to assert SET_QUERY_PARAM calls more robustly (filtering by message type) during navigation (L2043-L2112, L5341-L5365).

  • Gaps / missing edge-case coverage:

    • There’s no test that exercises the “all invalid values” path for setDoubleArrayValue with fromUi: false (backend-originated), which is the path most likely to cause a subtle state mismatch.
    • There’s no test that covers duplicate paramKey registration (two widgetIds bound to the same key) and the expected behavior (reject/overwrite/cleanup).
  • E2E tests: None added. Given this PR is “infrastructure” and not full user-facing behavior yet, unit tests may be sufficient for now, but I’d expect an E2E test once a concrete widget binding ships (to validate URL updates + navigation across pages in a real browser).

Backwards Compatibility

  • Mostly additive: New APIs on WidgetStateManager are additive and shouldn’t affect existing widgets unless bindings are registered.

  • Behavioral change risk: The setDoubleArrayValue change (filter + early return) can change runtime behavior in cases where invalid values occur. Even if “invalid values” are not expected from the backend, this can cause hard-to-debug issues if any caller ever passes [NaN] / [null] / [undefined] (or mixed) and expects the widget state manager to update consistently.

    • File reference: frontend/lib/src/WidgetStateManager.ts L607-L615.
  • setValue semantics: useBasicWidgetState now uses el.setValue as the explicit signal to use current proto value as the initial default (frontend/lib/src/hooks/useBasicWidgetState.ts L188-L200). This looks like the correct direction (avoids inferring seeding from value !== default), and is paired with tests.

Security & Risk

  • No obvious security issues (no new network calls, no sensitive data handling).
  • Regression risk is mainly around URL/query-string manipulation and state synchronization:
    • URL encoding/decoding correctness across repeated vs comma formats.
    • Ensuring backend-driven widget updates do not accidentally trigger URL updates (fromUi gating is good and tested).
    • The early-return in setDoubleArrayValue is the highest-risk change in this PR.

Recommendations

  1. Fix setDoubleArrayValue “all invalid values” behavior so it does not skip state updates for backend-originated changes and does not silently drop updates. Concretely:
    • frontend/lib/src/WidgetStateManager.ts L607-L615: avoid unconditional early-return; consider:
      • Only early-return when source.fromUi is true and the intention is only to clear URL params, or
      • Store an empty DoubleArray (or delete the widget state) and still call onWidgetValueChanged so the rest of the pipeline remains consistent.
  2. Add/adjust unit tests for the above:
    • Exercise the “all invalid values” path with fromUi: false and assert WidgetStateManager updates/flush behavior is correct (and that it does not update the URL).
  3. Define behavior for duplicate paramKey bindings:
    • frontend/lib/src/WidgetStateManager.ts L1102-L1110: either prevent duplicates (throw/log + ignore) or explicitly overwrite and clean up the prior binding so boundWidgets/paramKeyToWidgetId stay consistent.

Verdict

CHANGES REQUESTED: The query-param binding infrastructure looks solid and well-tested overall, but setDoubleArrayValue’s “all invalid values” early return is a regression risk that should be addressed before merge.


This is an automated AI review using gpt-5.2-high. Please verify the feedback and use your judgment.

@github-actions github-actions bot added the do-not-merge PR is blocked from merging label Feb 3, 2026
Copy link
Copy Markdown
Collaborator

@sfc-gh-bnisco sfc-gh-bnisco left a comment

Choose a reason for hiding this comment

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

+1 to the most recent AI review pass. I have small perf suggestion inline as well.

// Pure: Check if we should clear the param
const shouldClear = this.shouldClearUrlParam(urlValue, binding)

// Side effect: Update URL
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

suggestion (non-blocking): If the URL doesn't actually change, it might make sense to skip the underlying call to replaceState for performance reasons.

@mayagbarnes
Copy link
Copy Markdown
Collaborator Author

mayagbarnes commented Feb 3, 2026

CHANGES REQUESTED: The query-param binding infrastructure looks solid and well-tested overall, but setDoubleArrayValue’s “all invalid values” early return is a regression risk that should be addressed before merge.

I added logging when all values are filtered out, but I'm not sure this poses a "regression risk" - invalid values can't come from the UI, and if the backend sends them it would indicate an unhandled edge case in user code.
I chose to preserve previous valid state rather than store an empty array because setDoubleArrayValue is only used by the Slider, which requires at least one value - storing empty would cause rendering issues.

@mayagbarnes mayagbarnes removed the do-not-merge PR is blocked from merging label Feb 3, 2026
@mayagbarnes mayagbarnes merged commit 2e71790 into develop Feb 4, 2026
43 of 44 checks passed
@mayagbarnes mayagbarnes deleted the query-param-bind-2 branch February 4, 2026 06:24
@mayagbarnes mayagbarnes added impact:internal PR changes only affect internal code and removed impact:users PR changes affect end users labels Feb 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:feature PR contains new feature or enhancement implementation impact:internal PR changes only affect internal code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants