Issue 1 (#10598) - joaooliveira-11/streamlit GitHub Wiki

Avoid StreamlitDuplicateElementId error when the same widget is in the main area and sidebar

The Issue

The issue can be found at https://github.com/streamlit/streamlit/issues/10598. It is a request for an enhancement that the application does not support at the moment. When the same Streamlit widget, like st.button("Button"), is used both in the main area and the sidebar, a StreamlitDuplicateElementId error is thrown. This happens because Streamlit auto-generates an internal ID for each widget based on its type and parameters. When these are identical in two locations, the IDs clash. The only current workaround to avoid this error when using identical widgets is to explicitly provide a unique key, which Streamlit uses as part of the internal ID generation process.

Issue in practice

import streamlit as st

st.button("Button")
st.sidebar.button("Button")

image

Why Does This Issue Matter?

Suppose a user wants to place the same button in both the sidebar and the main area using just a label. Streamlit will raise an error unless the user explicitly provides a unique key. However, in cases where widgets are placed in distinct layout regions—such as the main content and the sidebar—it would be reasonable for Streamlit to automatically generate different internal keys based on their location. This would eliminate the need for manual key assignment in common use cases and improve the developer experience.

Requirements

This issue occurs in the context of a web-based application built using Streamlit, which is a Python framework for quickly building and deploying interactive web apps, particularly for data science and machine learning workflows.

Feature Context

Streamlit allows users to build UI components like buttons, sliders, and inputs using simple Python functions. These can be placed in two main layout areas:

  • Main content area
  • Sidebar area

User Story

  • As a User, I want to place the same widget in both the main area and the sidebar using the same label, without needing to manually assign unique keys, so that I can streamline layout duplication and improve my development speed.

Expected Behaviour

image

Source Code Files

This issue required modifications in two main areas of the source code: the individual widget calls to the internal ID generation function, and the function responsible for generating these IDs. Additionally, new tests were created to address this issue.

Direct files involved:

Backend Classes:
  • elements/widgets — updated compute_and_register_element_id() function call by adding a new active_dg_root_container argument.
  • arrow.py — updated compute_and_register_element_id() function call by adding a new active_dg_root_container argument.
  • deck_gl_json_chart.py — updated compute_and_register_element_id() function call by adding a new active_dg_root_container argument.
  • plotly_chart.py — updated compute_and_register_element_id() function call by adding a new active_dg_root_container argument.
  • vega_charts.py — updated compute_and_register_element_id() function call by adding a new active_dg_root_container argument.
  • utils.py — updated compute_and_register_element_id() function to accept a new parameter active_dg_root_container, which is now conditionally used in generating the unique internal ID.
  • widgets_test.py — added a new unit test test_not_triggers_duplicate_id_error(), to confirm that the duplicate ID issue is resolved.

Indirect files involved:

Backend Classes:
  • delta_generator.py — This file was used to retrieve the DeltaGenerator that's currently 'active' with its root container.

Design of the Fix

To address this feature, João Oliveira investigated how Streamlit generates internal IDs for each element. Through research, he determined that the compute_and_register_element_id() function calculates these IDs based on widget parameters, including user keys.

Initially, the design focused on identifying the element's location within the compute_and_register_element_id() function. João attempted to retrieve the last DeltaGenerator added to the application's dg_stack using get_last_dg_added_to_context_stack(). This approach aimed to pinpoint the active DeltaGenerator responsible for managing elements, thus revealing the location.

Although this solution seemed straightforward, João encountered implementation challenges. The dg_stack is updated exclusively within with blocks, which were not utilized in the issue's example. Consequently, this design proved unfeasible.

Following the initial setback, João explored an alternative: determining the location within the code where each widget invokes compute_and_register_element_id(). This became viable because every widget has a corresponding DeltaGenerator instance, allowing access to the location information at the call.

Within compute_and_register_element_id(), this new parameter is now conditionally used when generating the widget’s internal ID. Specifically, if no user key is provided and the widget resides in the sidebar, the location is included in the kwargs_to_use— the set of parameters used to compute a unique ID — ensuring that widgets with identical arguments but different locations no longer collide.

Diagram of the change

image

Fix Source Code

The fix modified 23 source code files, encompassing 21 widget-related files within lib/streamlit/elements/widgets/, along with lib/streamlit/elements/arrow.py, lib/streamlit/elements/deck_gl_json_chart.py, lib/streamlit/elements/plotly_chart.py, lib/streamlit/elements/vega_charts.py; a unit test at lib/tests/streamlit/runtime/state/widgets_test.py; and lib/streamlit/elements/lib/utils.py, which contains the core function of the fix.

Source code changes in lib/streamlit/elements/widgets/, lib/streamlit/elements/arrow.py, lib/streamlit/elements/deck_gl_json_chart.py, lib/streamlit/elements/plotly_chart.py and lib/streamlit/elements/vega_charts.py:

This change consisted in adding a new argument to the compute_and_register_element_id() that specifies the location of the widget to be created that will be used to compute a unique internal ID.

image

image

image

image

image

image

Source code changes in lib/streamlit/elements/lib/utils.py:

This change consisted in adding a new parameter to the compute_and_register_element_id() that specifies the location of the widget to be created that will be used to compute a unique internal ID. This location is then conditionally incorporated into the unique internal ID calculation if no user key is provided and the element resides in the sidebar.

image

Source code changes in lib/tests/streamlit/runtime/state/widgets_test.py:

This change included adding a new unit test to verify the fix. The test involves adding every widget currently available in Streamlit to both the sidebar and the main area simultaneously, using the same arguments for each instance.

image

Submit The Fix

A pull request was submitted on the project's Github and can be found at https://github.com/streamlit/streamlit/pull/10881. Although not yet merged, it has received a suggestion from a core contributor and successfully passed all tests in the CI/CD pipeline.

Update 28/05: PR Merged.