Skip to content

Bind widgets to query params - FE hooks & color_picker support#13845

Merged
mayagbarnes merged 6 commits intodevelopfrom
binding-hooks
Feb 10, 2026
Merged

Bind widgets to query params - FE hooks & color_picker support#13845
mayagbarnes merged 6 commits intodevelopfrom
binding-hooks

Conversation

@mayagbarnes
Copy link
Copy Markdown
Collaborator

@mayagbarnes mayagbarnes commented Feb 6, 2026

Describe your changes

This PR introduces a reusable frontend hook for query‑param binding and adds the bind parameter to st.color_picker to enable two-way sync between widget values and URL query parameters.

  • URL seeding - Widget initializes from URL on page load
  • URL sync - user interaction updates the URL
  • useQueryParamBinding hook - Extracted reusable hook for registering/unregistering query param bindings
  • Default cleanup - param removed when value returns to default
  • Invalid URL handling — invalid values are cleared (not corrected to default)
  • Bind validation — StreamlitInvalidBindValueError for unsupported bind values

st.color_picker:

  • Hex validation - accepts 3/6‑char hex with or without #, normalize to include # prefix (supports both #RGB and #RRGGBB formats, case preserved)

Also, for hardening:

  • Empty widget keys now raise an error: Previously, passing key="" to any widget
    was silently accepted (though non-functional). This PR adds validation that raises
    StreamlitAPIException if an empty string is provided as a widget key. This is a
    defensive hardening to catch likely user errors early.

Testing Plan

  • Python/JS Unit Tests: ✅ Added
  • E2E Tests: ✅ Added
  • Manual Testing: ✅

@mayagbarnes mayagbarnes added security-assessment-completed change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users labels Feb 6, 2026
@snyk-io
Copy link
Copy Markdown
Contributor

snyk-io bot commented Feb 6, 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 Feb 6, 2026

✅ PR preview is ready!

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

@mayagbarnes mayagbarnes requested a review from Copilot February 6, 2026 21:36
@mayagbarnes mayagbarnes changed the title Bind widgets to query params - Hooks & color picker support Bind widgets to query params - FE hooks & color_picker support Feb 6, 2026
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 adds URL query-parameter binding support for widgets by introducing a reusable frontend hook for registering query-param bindings, extending the ColorPicker protobuf + widget implementation to carry a query-param key, and adjusting session-state URL seeding/validation behavior for bound widgets.

Changes:

  • Add query_param_key to the ColorPicker protobuf and plumb bind="query-params" through st.color_picker to the frontend.
  • Introduce useQueryParamBinding and integrate optional query-param binding registration into useBasicWidgetState.
  • Extend backend session-state URL seeding logic and add unit/E2E coverage for bind validation and URL seeding/sync.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
proto/streamlit/proto/ColorPicker.proto Adds a proto field to send the query-param binding key to the frontend.
lib/streamlit/elements/widgets/color_picker.py Adds bind param support, query-param key plumbing, and updated color deserialization/validation.
lib/streamlit/runtime/state/widgets.py Validates bind values and enforces binding preconditions (including clearable).
lib/streamlit/runtime/state/session_state.py Updates URL seeding behavior for bound widgets, including new invalid-value clearing heuristics.
lib/streamlit/runtime/state/common.py Adds consistent empty-key validation via require_valid_user_key.
lib/streamlit/elements/lib/policies.py Enforces key validity at widget policy-check time.
lib/streamlit/errors.py Introduces a dedicated exception for invalid bind values.
lib/tests/streamlit/runtime/state/widgets_test.py Adds tests for invalid bind values and widget binding policy behavior.
lib/tests/streamlit/runtime/state/session_state_test.py Adds coverage for URL seeding behavior when invalid URL values are encountered.
lib/tests/streamlit/elements/color_picker_test.py Adds coverage for bind behavior and ColorPicker serde normalization behavior.
frontend/lib/src/hooks/useQueryParamBinding.ts New hook to register/unregister widget query-param bindings.
frontend/lib/src/hooks/useQueryParamBinding.test.ts Unit tests for the new binding hook.
frontend/lib/src/hooks/useBasicWidgetState.ts Integrates optional query-param binding registration and memoization to reduce reruns.
frontend/lib/src/hooks/useBasicWidgetState.test.ts Adds tests for query-param binding integration in useBasicWidgetState.
frontend/lib/src/components/widgets/ColorPicker/ColorPicker.tsx Wires proto queryParamKey into query-param binding registration.
frontend/lib/src/components/widgets/ColorPicker/ColorPicker.test.tsx Adds tests for ColorPicker query-param binding registration/unregistration.
e2e_playwright/st_color_picker.py Adds bound ColorPicker examples used by E2E tests.
e2e_playwright/st_color_picker_test.py Adds E2E coverage for seeding, URL updates, default cleanup, and invalid URL handling.

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

github-actions bot commented Feb 6, 2026

Summary

This PR introduces a reusable frontend useQueryParamBinding hook and integrates it into the existing useBasicWidgetState hook, then adds the bind="query-params" parameter to st.color_picker as the first widget to support two-way URL query parameter synchronization. The changes span the full stack: protobuf schema, Python backend, React frontend hooks, and comprehensive tests at all levels (unit + E2E).

Key changes:

  • New useQueryParamBinding hook — registers/unregisters widget bindings with WidgetStateManager, with proper cleanup on unmount.
  • useBasicWidgetState integration — accepts an optional queryParamBinding config, with careful memoization to prevent unnecessary effect re-runs.
  • st.color_picker bind support — adds bind: BindOption = None keyword-only parameter, proto field query_param_key, and hex validation in the serde.
  • Invalid value handling_seed_widget_from_url now detects when a deserializer silently falls back to default for invalid URL values and clears the param.
  • Validation hardeningStreamlitInvalidBindValueError for bad bind values, empty key rejection in require_valid_user_key.

Code Quality

Frontend — Well-structured and idiomatic React:

  • The useQueryParamBinding hook follows the Effect checklist: it syncs with an external system (WidgetStateManager), has exactly one concern, includes cleanup, and has a complete dependency array. Good.
  • The memoization in useBasicWidgetState (lines 259–279 of useBasicWidgetState.ts) is thoughtfully done — defaultValueForBinding uses useMemo keyed on element, and queryParamBindingOptions uses JSON.stringify for value-based array comparison. The eslint-disable comments explain the rationale.
  • The QueryParamBindingConfig interface in useBasicWidgetState.ts is well-documented and forward-looking (includes urlFormat and optionStrings for future widgets).

Potential issue — removed eslint-disable comment:

In useBasicWidgetState.ts line 296, the mutation element.setValue = false previously had an eslint-disable comment for react-hooks/immutability. The diff shows this comment was removed:

-    // eslint-disable-next-line react-hooks/immutability -- TODO: Update to match React best practices
     element.setValue = false // Clear "event".

This may introduce a new lint warning/error. Please verify this doesn't break the lint step, or restore the disable comment if the rule is still active.

Backend Python — Clean and follows conventions:

  • _HEX_COLOR_RE is compiled at module level with a leading underscore — follows the module-private convention.
  • ColorPickerSerde.deserialize is enhanced to validate hex colors and normalize the # prefix, which is essential for graceful handling of URL-seeded values.
  • The StreamlitInvalidBindValueError follows the existing LocalizableStreamlitException pattern exactly.

Proto change — Backward compatible:

  • optional string query_param_key = 10; in ColorPicker.proto — field 10 is the next sequential number, optional correctly indicates presence semantics. Additive-only change; no existing fields are modified.

Test Coverage

Test coverage is excellent across all layers:

E2E Tests (5 new tests):

  • URL seeding from query params
  • URL updates on user interaction (including reset to default)
  • Custom default with URL override
  • Invalid URL value handling (cleared from URL)
  • 3-char hex shorthand support

All tests use expect (auto-wait) assertions, descriptive names, docstrings, and label-based locators via get_color_picker from app_utils. Negative assertions are present (e.g., verifying query param absence when value returns to default, verifying invalid params are removed).

Frontend Unit Tests:

  • useQueryParamBinding.test.ts (7 tests): register, unregister, unmount cleanup, null key, options passthrough, re-registration on key change.
  • useBasicWidgetState.test.ts (4 new tests): integration with binding config, no-binding, unmount, options passthrough.
  • ColorPicker.test.tsx (4 new tests): register on mount, unregister on unmount, no-op without key, custom default.

Python Unit Tests:

  • color_picker_test.py: bind sets proto field, bind without key raises, no-bind leaves proto empty, invalid bind raises, empty key raises, serde validation.
  • session_state_test.py: invalid URL value detection and clearing.
  • widgets_test.py: invalid bind value validation, bind excluded from element ID computation.

Minor suggestions for additional coverage:

  1. A TestColorPickerSerde test for the serialize method (trivial but documents round-trip behavior).
  2. A test verifying that ColorPickerSerde.deserialize("") returns the default (the empty string path).

Backwards Compatibility

Low risk overall, but two changes affect all widgets:

  1. check_widget_policies now calls require_valid_user_key(key) for all widgets (policies.py line 178-179). This means key="" will now raise a StreamlitAPIException for any widget, not just st.color_picker. While key="" is almost certainly a user bug (it would cause undefined behavior), this is technically a breaking change for anyone passing empty string keys. This is a reasonable hardening that prevents subtle bugs.

  2. _seed_widget_from_url has new invalid-value detection (session_state.py lines 1028-1039). This affects all widgets with bind="query-params". The logic is sound: if the deserializer silently falls back to default for a non-default URL value, the param is cleared. The three-part condition (deserialized_value == default_value and parsed_value != serialized_default and not is_empty_url_value(url_value)) correctly distinguishes "invalid value fell back to default" from "user explicitly set the default value in the URL". However, this could be a subtle behavior change for any future widgets where multiple valid URL representations map to the default value but don't round-trip through serializer(deserializer(None)).

The bind parameter itself is keyword-only (after *), defaults to None, and has no effect when not set — fully backward compatible.

Security & Risk

  • No security concerns identified. The hex color validation regex is correct and prevents injection of arbitrary strings. Invalid URL values are sanitized rather than passed through.
  • Regression risk is low. The ColorPickerSerde.deserialize change is more restrictive than before (validates instead of blindly converting), which is safer. The previous behavior of str(ui_value) would have returned the raw string for any input — now invalid inputs fall back to default.
  • The _seed_widget_from_url change adds an extra metadata.deserializer(None) and metadata.serializer(default_value) call per seeding operation. This is negligible overhead.

Accessibility

No accessibility concerns. The color picker's UI is unchanged — only the data binding layer is modified. The widget continues to use the same BaseColorPicker component with proper labeling and keyboard interaction.

Recommendations

  1. Verify the eslint-disable removal: In useBasicWidgetState.ts, the react-hooks/immutability disable comment was removed from the element.setValue = false mutation (around line 296). If this lint rule is still active, this will cause a lint failure. Either restore the disable comment or address the TODO to avoid the mutation.

  2. Consider adding a JSDoc note about referential stability for useQueryParamBinding: The options parameter is an object in the useEffect dependency array. While the current caller (useBasicWidgetState) properly memoizes it, direct callers of useQueryParamBinding need to be aware that passing a new object reference each render will cause unnecessary effect re-runs. A brief note in the JSDoc (e.g., "The options object should be memoized or stable across renders") would help future consumers.

  3. Add serde tests for edge cases: Consider adding ColorPickerSerde unit tests for:

    • deserialize("") returns the default value
    • serialize round-trips correctly with deserialize
    • deserialize with a value like "##000000" (double hash) falls back to default
  4. Document the broader impact of check_widget_policies change: The empty-key validation in policies.py affects all widgets globally. While this is a good hardening, it might be worth calling this out in the PR description or changelog since it could affect users passing key="" to any widget.

Verdict

APPROVED: This is a well-structured, thoroughly tested PR that introduces clean infrastructure for query param binding and applies it to st.color_picker as the first widget. The code follows existing patterns, test coverage is comprehensive across all layers, and the changes are backward compatible. The minor issues identified (eslint-disable removal, referential stability documentation) are non-blocking and can be addressed in follow-up if needed.


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

@mayagbarnes mayagbarnes marked this pull request as ready for review February 6, 2026 22:42
@streamlit streamlit deleted a comment from github-actions bot Feb 7, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 7, 2026

Summary

Adds query-param binding for st.color_picker, wires frontend registration through a reusable hook, hardens URL seeding/clearing logic, and expands unit + e2e coverage for the new behavior.

Code Quality

Overall structure is clean and consistent with existing widget patterns. One style issue: new Python tests added without -> None return annotations, which conflicts with the Python test guide (e.g., lib/tests/streamlit/elements/color_picker_test.py:236-263).

Test Coverage

Coverage is solid across backend, frontend hooks, and e2e scenarios (seeding, URL updates, invalid values, defaults). One e2e test does not include a negative “must NOT happen” assertion as recommended in the e2e guide (e2e_playwright/st_color_picker_test.py:202-210).

Backwards Compatibility

bind is optional and defaults to None, so existing code paths remain unchanged. The new validation will raise on invalid bind values, which is expected behavior and should be low-risk.

Security & Risk

No security concerns identified. URL normalization/cleanup is localized to query-param binding and appears low risk.

Accessibility

No UI or interaction changes beyond URL syncing; accessibility behavior should be unchanged.

Recommendations

  1. Add a negative assertion to the query-param seeding e2e test to align with e2e best practices (e2e_playwright/st_color_picker_test.py:202-210).
  2. Add explicit -> None return types to new Python tests to match the test guide (lib/tests/streamlit/elements/color_picker_test.py:236-263).
  3. Since a proto field was added, ensure protobuf artifacts are regenerated if this repo tracks them (proto/streamlit/proto/ColorPicker.proto:21-32).

Verdict

APPROVED: Feature is well-implemented and tested; only minor best-practice nits remain.


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

@mayagbarnes mayagbarnes merged commit 847c949 into develop Feb 10, 2026
43 checks passed
@mayagbarnes mayagbarnes deleted the binding-hooks branch February 10, 2026 06:25
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:users PR changes affect end users

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants