Skip to content

[feature] Sync URL when session_state updates query-param-bound widgets#14744

Merged
sfc-gh-lwilby-1 merged 13 commits into
developfrom
fix/gh-14670
May 12, 2026
Merged

[feature] Sync URL when session_state updates query-param-bound widgets#14744
sfc-gh-lwilby-1 merged 13 commits into
developfrom
fix/gh-14670

Conversation

@sfc-gh-lwilby

@sfc-gh-lwilby sfc-gh-lwilby commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Describe your changes

Programmatic st.session_state[key] = value for widgets with bind="query-params" now updates the backend query string and emits page_info_changed, so the browser URL matches the widget and reload preserves the value. Resets to the widget default use remove_param (with ForwardMsg) when the change came from session state in the current run, instead of silently discarding backend state only.

Comparison uses the same normalization as set_corrected_value (extracted to _coerce_value_for_query_url) so reruns that set session state to the URL-equivalent value do not spam ForwardMsgs.

Screenshot or video (only for visual changes)

N/A (URL bar behavior; covered by E2E).

GitHub Issue Link (if applicable)

Implements #14670

Testing Plan

  • Unit Tests (JS and/or Python)
  • E2E Tests
  • Any manual testing needed?

Python: RegisterWidgetQueryParamProgrammaticSyncTest in session_state_test.py. E2E: test_text_input_query_param_programmatic_session_state_syncs_url in st_text_input_test.py.

Contribution License Agreement

By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.

Made-with: Cursor

Co-authored-by: lawilby <laura.wilby+oss@snowflake.com>
@sfc-gh-lwilby sfc-gh-lwilby added change:bugfix PR contains bug fix implementation impact:users PR changes affect end users labels Apr 13, 2026
@snyk-io

snyk-io Bot commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

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

Status Scan Engine 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

github-actions Bot commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

✅ PR preview is ready!

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

sfc-gh-lwilby-1 and others added 2 commits April 24, 2026 17:38
- Update test_skips_url_sync_when_session_state_set to assert
  set_corrected_value IS called (matching gh-14670 behavior)
- Bump TEXT_INPUT_ELEMENTS from 22 to 23 for new gh14670 text input
- Guard gh-14670 e2e section with runtime.exists() for bare execution

Made-with: Cursor

Co-authored-by: lawilby <laura.wilby+oss@snowflake.com>
Comment thread lib/streamlit/runtime/state/session_state.py Fixed
@sfc-gh-lwilby-1 sfc-gh-lwilby-1 marked this pull request as ready for review April 24, 2026 16:58
@greptile-apps

greptile-apps Bot commented Apr 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes gh-14670 by making programmatic st.session_state[key] = value writes for bind="query-params" widgets emit a page_info_changed ForwardMsg so the browser URL stays in sync and reloads preserve the set value. Resets to the widget default now call remove_param (with a ForwardMsg) instead of the silent discard_param_no_forward_msg when the change came from session state in the current run. The fix is gated by user_key in _new_session_state and uses a new stored_param_matches_corrected_value helper to suppress redundant messages when the URL already matches. Coverage is thorough: 7 new unit tests exercise the programmatic-set, reset, idempotent-skip, UI-driven exclusion, and type-coercion paths, plus an E2E test that verifies the URL bar and reload behaviour.

Confidence Score: 5/5

Safe to merge; remaining findings are P2 style nits with no correctness impact.

Logic is correct and well-tested end-to-end. The two remaining comments are style nits (redundant elif condition and an unreachable flag assignment) that do not affect runtime behaviour.

No files require special attention.

Important Files Changed

Filename Overview
lib/streamlit/runtime/state/session_state.py Core fix: programmatic session_state sets for bind=query-params widgets now sync the URL via set_corrected_value; reset-to-default uses remove_param (with ForwardMsg) instead of the silent discard. Two minor style redundancies (elif vs else, spurious restored_bound_value flag).
lib/streamlit/runtime/state/query_params.py Refactors number/value coercion into module-level helpers _format_number_for_query_url and _coerce_value_for_query_url (extracted from set_corrected_value), and adds stored_param_matches_corrected_value for idempotent URL-sync checks. Behaviour of set_corrected_value is unchanged.
lib/tests/streamlit/runtime/state/session_state_test.py Adds RegisterWidgetQueryParamProgrammaticSyncTest with 7 focused unit tests covering the new URL-sync path (set, reset-to-default, idempotent skip, UI-driven exclusion, float/bool/array coercion). Updates test_syncs_url_when_session_state_set to assert set_corrected_value is now called.
e2e_playwright/st_text_input_test.py Adds E2E test verifying programmatic session_state updates sync to the browser URL, that reload preserves the value, and that reset-to-default removes the query param.
e2e_playwright/st_text_input.py Adds the gh-14670 fixture: a bind=query-params text_input with buttons to set/reset it via session_state, used by the new E2E test.

Reviews (1): Last reviewed commit: "fix: resolve CI failures in test asserti..." | Re-trigger Greptile

Comment thread lib/streamlit/runtime/state/session_state.py Outdated
Comment thread lib/streamlit/runtime/state/session_state.py Outdated
- Replace `elif widget_value == default_value` with `else` (always true
  after the preceding `if widget_value != default_value`)
- Remove redundant `restored_bound_value = True` in programmatic-set
  branch (already implied by `is_new_state_value`)

Made-with: Cursor

Co-authored-by: lawilby <laura.wilby+oss@snowflake.com>
Copilot AI review requested due to automatic review settings April 24, 2026 17:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review is ineligible. To be eligible to request a review, you need a paid Copilot license, or your organization must enable Copilot code review.

Comment thread e2e_playwright/st_text_input.py Outdated
@sfc-gh-lwilby-1 sfc-gh-lwilby-1 changed the title [fix] Sync URL when session_state updates query-param-bound widgets [feature] Sync URL when session_state updates query-param-bound widgets Apr 24, 2026
Comment thread e2e_playwright/st_text_input_test.py Outdated
@sfc-gh-lwilby-1 sfc-gh-lwilby-1 added the ai-review If applied to PR or issue will run AI review workflow label Apr 24, 2026
URL seeding via _handle_query_param_binding puts values into
_new_session_state, which the programmatic sync branch incorrectly
treated as explicit st.session_state assignments. This caused
date/time slider params to be re-serialized in the wrong format
and empty-override params for multiselect/pills to be dropped.

Guard the programmatic sync branches with `not url_value_seeded` so
only genuine st.session_state["key"] = value assignments trigger
URL updates. Update slider E2E test to expect URL sync for
session_state pre-sets per gh-14670.

Made-with: Cursor

Co-authored-by: lawilby <laura.wilby+oss@snowflake.com>
@github-actions github-actions Bot removed the ai-review If applied to PR or issue will run AI review workflow label Apr 24, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Summary

This PR fixes #14670: programmatic st.session_state[key] = value updates for widgets with bind="query-params" now correctly synchronize the browser URL via a page_info_changed ForwardMsg. Previously, such updates changed the widget value internally but left the URL stale, causing the programmatic value to be lost on page reload. Resetting to the widget default now also removes the query parameter from the URL.

Key changes:

  • session_state.py: Extended the register_widget query-param sync block with two new branches — one for programmatic sets that differ from the current URL, and one for programmatic resets to default that actively remove the param (with a ForwardMsg).
  • query_params.py: Extracted inline formatting logic into module-level helpers (_format_number_for_query_url, _coerce_value_for_query_url) and added stored_param_matches_corrected_value for deduplication to avoid redundant ForwardMsgs.
  • Tests: 8 new unit tests in RegisterWidgetQueryParamProgrammaticSyncTest, an updated remount test, and a new E2E test validating the full lifecycle (set → reload persistence → reset to default).

All three reviewers (claude-4.6-opus-high-thinking, gemini-3.1-pro, gpt-5.3-codex-high) completed their reviews successfully.

Code Quality

Unanimous agreement across all reviewers. The code is well-structured, follows existing patterns, and the refactoring of set_corrected_value into reusable helpers is clean and reduces duplication. The branching logic in register_widget is clearly commented with rationale for each path. Helper functions are correctly module-private (_ prefix), type-annotated, and documented.

Test Coverage

Unanimous agreement: coverage is strong and comprehensive.

  • Unit tests (8 new methods): Cover programmatic set with URL sync and ForwardMsg, reset-to-default removal, deduplication when URL already matches, UI-driven exclusion from programmatic sync path, and type coercion for double_value, bool_value, and string_array_value. The existing remount test was updated to verify the corrected behavior.
  • E2E test: test_text_input_query_param_programmatic_session_state_syncs_url validates the full user-facing lifecycle using expect auto-wait assertions and expect_prefixed_markdown for value verification.
  • Minor gap (gpt-5.3-codex-high): An optional double_array_value programmatic equivalence test could further guard URL-coercion edge cases. Non-blocking.

Backwards Compatibility

Unanimous agreement: no backwards compatibility issues. The behavior change is intentional — programmatic session-state updates for query-param-bound widgets now keep the URL in sync. No API signatures change and no previously valid code breaks. This aligns with user expectations (the issue reporter called the old behavior a "blocker").

Security & Risk

Unanimous agreement: no security concerns. No new endpoints, routes, or WebSocket behavior changes. No changes to authentication, CSRF, cookies, or session management. No new dependencies. Values are URL-encoded via urllib.parse.urlencode. The changes are confined to the existing page_info_changed ForwardMsg mechanism.

Regression risk is low: the core logic change is narrowly scoped to the user_key in self._new_session_state condition, which only fires for programmatic session-state updates in the current run.

External test recommendation

  • Recommend external_test: No (2-1 majority)
  • Agreement: claude-4.6-opus-high-thinking and gpt-5.3-codex-high both assessed no external test needed, citing that changes are confined to internal query-param sync logic within the existing ForwardMsg mechanism with no routing, auth, or WebSocket changes.
  • Disagreement: gemini-3.1-pro recommended external tests targeting categories "Routing and URL behavior" and "Embedding and iframe boundary," suggesting verification of postMessage sync in iframe embeddings.
  • Resolution: The changes modify backend-to-frontend ForwardMsg flow for page_info_changed, which is the same channel used by external/embedded apps. No new cross-origin, routing, or embedding behavior is introduced. The existing E2E test validates URL sync in a real browser. The external test recommendation is No, though the iframe postMessage scenario noted by gemini-3.1-pro could be worth verifying in a future infrastructure-focused effort if any embedding-related regressions surface.
  • Confidence: High

Accessibility

Unanimous agreement: no accessibility concerns. All changes are in Python backend code and test files — no frontend component rendering or interaction semantics were modified.

Recommendations

  1. (Low priority) Consider renaming test_programmatic_float_coercion_matches_url_integer_form to something like test_programmatic_float_scalar_matches_url_string_form — the current name suggests integer-form collapse, but the test actually verifies that scalar double_value uses str() (producing "5.0").
  2. (Optional follow-up) Add a unit test for double_array_value programmatic equivalence to further guard URL-coercion edge cases.

Verdict

APPROVED: Well-scoped bug fix with thorough unit and E2E test coverage, clean refactoring, correct behavioral change, and low regression risk. All three reviewers approved unanimously with no blocking issues identified.


Consolidated from reviews by claude-4.6-opus-high-thinking, gemini-3.1-pro, gpt-5.3-codex-high. Consolidation performed by claude-4.6-opus-high-thinking.

This review also includes 2 inline comment(s) on specific code lines.

Comment thread lib/tests/streamlit/runtime/state/session_state_test.py Outdated
Comment thread lib/streamlit/runtime/state/query_params.py
sfc-gh-lwilby-1 and others added 2 commits April 24, 2026 19:37
Made-with: Cursor

Co-authored-by: lawilby <laura.wilby+oss@snowflake.com>
- Rename gh14670 references in E2E tests to descriptive names
  (bound_text_ss, set_bound_text_ss_btn, etc.) instead of issue numbers
- Remove issue number from test docstring
- Rename test_programmatic_float_coercion_matches_url_integer_form to
  test_programmatic_float_scalar_matches_url_string_form per nitpick

Made-with: Cursor

Co-authored-by: lawilby <laura.wilby+oss@snowflake.com>
@sfc-gh-lwilby-1

Copy link
Copy Markdown
Contributor

Addressed all review comments in e46a3e9:

  • Renamed gh14670 references in E2E tests to descriptive names (bound_text_ss, set_bound_text_ss_btn, etc.)
  • Removed issue number from test docstring
  • Renamed test_programmatic_float_coercion_matches_url_integer_formtest_programmatic_float_scalar_matches_url_string_form

Flatten multi-line button calls to single lines per ruff formatter.

Made-with: Cursor

Co-authored-by: lawilby <laura.wilby+oss@snowflake.com>
Comment thread lib/tests/streamlit/runtime/state/session_state_test.py Outdated
Comment thread lib/tests/streamlit/runtime/state/session_state_test.py Outdated
Comment thread e2e_playwright/st_slider_test.py Outdated
sfc-gh-lwilby-1 and others added 2 commits April 28, 2026 09:55
Address reviewer nit from @mayagbarnes — reference format consistency.

Made-with: Cursor

Co-authored-by: lawilby <laura.wilby+oss@snowflake.com>
Keep only the implementation comment reference in session_state.py;
the test names and docstrings are self-descriptive.

Made-with: Cursor

Co-authored-by: lawilby <laura.wilby+oss@snowflake.com>
Comment thread lib/streamlit/runtime/state/session_state.py Outdated
Comment thread e2e_playwright/st_slider_test.py Outdated
Comment thread lib/tests/streamlit/runtime/state/session_state_test.py

@mayagbarnes mayagbarnes left a comment

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.

Just the logic change/unit tests suggestions above, other LGTM!

Address @mayagbarnes review: add _old_state check so the programmatic
sync branch only fires after the widget has been registered at least
once, preventing app-initialization values from polluting fresh URLs.

- Add (widget_id in _old_state or user_key in _old_state) guard to
  both the programmatic-set and reset-to-default branches
- Revert slider E2E test to expect no URL sync on initial pre-set
- Add test_initial_load_preseeding_does_not_sync_url
- Add test_url_seeded_initial_load_does_not_trigger_programmatic_sync

Co-authored-by: Cursor <cursoragent@cursor.com>

Co-authored-by: lawilby <laura.wilby+oss@snowflake.com>
@JosephMarinier

Copy link
Copy Markdown
Contributor

Thank you very much for tackling this! ❤️

As I said when I filed #14670, I was thrilled by the new bind="query-params", which I hope will 1) replace a bunch of hacky code I had syncing widgets with query params and 2) make feature development easier, but that issue is a blocker for me to adopt it.

Is there anything I can do to help?

sfc-gh-lwilby-1 and others added 2 commits May 12, 2026 13:08
Resolved trivial concatenation conflicts in e2e_playwright/st_text_input.py
and e2e_playwright/st_text_input_test.py — the PR's bound_text_ss session-state
sync harness/test and develop's setvalue_test_input element-hash-memo
regression test live side-by-side at the end of each file.

Co-authored-by: Cursor <cursoragent@cursor.com>

Co-authored-by: lawilby <laura.wilby+oss@snowflake.com>
Both this PR and develop's #15008 independently added a new st.text_input
to the harness and each bumped TEXT_INPUT_ELEMENTS from 22 to 23. After
the merge the harness has two extra text inputs (bound_text_ss and
setvalue_test_input) but the constant was only bumped once. Fix by
bumping to 24 to match the actual rendered count.

Co-authored-by: Cursor <cursoragent@cursor.com>

Co-authored-by: lawilby <laura.wilby+oss@snowflake.com>
@sfc-gh-lwilby-1 sfc-gh-lwilby-1 merged commit 26b0cdb into develop May 12, 2026
40 checks passed
@sfc-gh-lwilby-1 sfc-gh-lwilby-1 deleted the fix/gh-14670 branch May 12, 2026 12:15
@sfc-gh-lwilby-1

Copy link
Copy Markdown
Contributor

@JosephMarinier thanks for the nudge, this PR was ready to merge so I've merged it. We should have a new release in ~2 weeks, but also you can use the nightly to use the feature earlier!

lukasmasuch pushed a commit that referenced this pull request May 12, 2026
…ts (#14744)

## Describe your changes

Programmatic `st.session_state[key] = value` for widgets with
`bind="query-params"` now updates the backend query string and emits
`page_info_changed`, so the browser URL matches the widget and reload
preserves the value. Resets to the widget default use `remove_param`
(with ForwardMsg) when the change came from session state in the current
run, instead of silently discarding backend state only.

Comparison uses the same normalization as `set_corrected_value`
(extracted to `_coerce_value_for_query_url`) so reruns that set session
state to the URL-equivalent value do not spam ForwardMsgs.

## Screenshot or video (only for visual changes)

N/A (URL bar behavior; covered by E2E).

## GitHub Issue Link (if applicable)

Implements #14670

## Testing Plan

- [x] Unit Tests (JS and/or Python)
- [x] E2E Tests
- [ ] Any manual testing needed?

Python: `RegisterWidgetQueryParamProgrammaticSyncTest` in
`session_state_test.py`. E2E:
`test_text_input_query_param_programmatic_session_state_syncs_url` in
`st_text_input_test.py`.


**Contribution License Agreement**

By submitting this pull request you agree that all contributions to this
project are made under the Apache 2.0 license.

---------

Co-authored-by: Laura Wilby <laura.wilby@snowflake.com>
Co-authored-by: lawilby <laura.wilby+oss@snowflake.com>
joanaarnauth pushed a commit to joanaarnauth/streamlit that referenced this pull request May 17, 2026
…ts (streamlit#14744)

## Describe your changes

Programmatic `st.session_state[key] = value` for widgets with
`bind="query-params"` now updates the backend query string and emits
`page_info_changed`, so the browser URL matches the widget and reload
preserves the value. Resets to the widget default use `remove_param`
(with ForwardMsg) when the change came from session state in the current
run, instead of silently discarding backend state only.

Comparison uses the same normalization as `set_corrected_value`
(extracted to `_coerce_value_for_query_url`) so reruns that set session
state to the URL-equivalent value do not spam ForwardMsgs.

## Screenshot or video (only for visual changes)

N/A (URL bar behavior; covered by E2E).

## GitHub Issue Link (if applicable)

Implements streamlit#14670

## Testing Plan

- [x] Unit Tests (JS and/or Python)
- [x] E2E Tests
- [ ] Any manual testing needed?

Python: `RegisterWidgetQueryParamProgrammaticSyncTest` in
`session_state_test.py`. E2E:
`test_text_input_query_param_programmatic_session_state_syncs_url` in
`st_text_input_test.py`.


**Contribution License Agreement**

By submitting this pull request you agree that all contributions to this
project are made under the Apache 2.0 license.

---------

Co-authored-by: Laura Wilby <laura.wilby@snowflake.com>
Co-authored-by: lawilby <laura.wilby+oss@snowflake.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:bugfix PR contains bug fix implementation impact:users PR changes affect end users

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants