From 90f9934a0287ad1a011e366bd090fe90e276aa12 Mon Sep 17 00:00:00 2001 From: Johannes Rieke Date: Mon, 29 Sep 2025 23:43:03 +0200 Subject: [PATCH 1/4] Add first version of step --- ...step-xxxx-dynamic-tabs-expander-popover.md | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 candidates/step-xxxx-dynamic-tabs-expander-popover.md diff --git a/candidates/step-xxxx-dynamic-tabs-expander-popover.md b/candidates/step-xxxx-dynamic-tabs-expander-popover.md new file mode 100644 index 0000000..00aec46 --- /dev/null +++ b/candidates/step-xxxx-dynamic-tabs-expander-popover.md @@ -0,0 +1,94 @@ +# Summary + +Make it possible for `st.tabs`, `st.expander`, and `st.popover` content to run lazily (only when opened/active), instead of always executing on every rerun. + +# Problem statement + +Currently, tabs, expanders and popovers always execute their content, even if not visible: + +```python +with st.expander("Show details"): + slow_function() # runs even if expander is closed + +tab1, tab2 = st.tabs(["One", "Two"]) +with tab2: + slow_function() # runs even if Tab 2 is not active +``` + +This ensures instant visibility when toggled, but it slows apps significantly when hidden content is expensive to compute. + +Requests: +- (230 πŸ‘) streamlit/streamlit#6004 +- (93 πŸ‘) streamlit/streamlit#2399 + +Related: +- (79 πŸ‘) streamlit/streamlit#8239 -> should explore in parallel in case there's some implementation overlap to this feature +- (68 πŸ‘) streamlit/streamlit#8265 -> probably separate but should keep an eye on this since it's also about controlling state +- (6 πŸ‘) streamlit/streamlit#6370 -> same, probably separate + + +# Proposed solution + +Add an `on_change`/`rerun` parameter to expanders and tabs that enables lazy execution. + +## API + +### Option 1: Boolean evaluation of container + +```python +exp = st.expander("Show details", on_change="rerun") +if exp: # True if open + with exp: + st.write("Expander is open") +``` + +```python +tab1, tab2 = st.tabs(["A", "B"], on_change="rerun") +if tab1: # True if active + with tab1: + st.write("Tab A active") +elif tab2: + with tab2: + st.write("Tab B active") +``` + +Pros: Clean, readable, minimal changes. +Cons: Slight β€œmagic” in truthiness of DGs. + +### Option 2: Session state value + +```python +st.tabs(["A", "B"], key="tabs", on_change="rerun") +if st.session_state.tabs == "A": + ... +``` + +Pros: Explicit, consistent with widgets. +Cons: Verbose, less ergonomic for common case. + +### Option 3: DG attribute + +```python +exp = st.expander("Expand me", on_change="rerun") +if exp.open: + with exp: + st.write("Content") + +tab1, tab2 = st.tabs(["A", "B"], on_change="rerun") +if tab1.open: + ... +``` + +Pros: Explicit, type-safe, extendable (`.update()` APIs later). +Cons: Slightly wordier than Option 1. + + +## Behavior + +* When `on_change="rerun"`, interaction triggers rerun. +* Only active container executes its code. +* Inactive containers are skipped, unless already cached or explicitly persisted. + +# Checklist + +TBD \ No newline at end of file From 7be9e67b2b4c811dad9ec99f82ad7dfa6e21dbe4 Mon Sep 17 00:00:00 2001 From: Johannes Rieke Date: Tue, 30 Sep 2025 00:05:24 +0200 Subject: [PATCH 2/4] Expand options --- ...step-xxxx-dynamic-tabs-expander-popover.md | 88 +++++++++++++++---- 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/candidates/step-xxxx-dynamic-tabs-expander-popover.md b/candidates/step-xxxx-dynamic-tabs-expander-popover.md index 00aec46..cca237b 100644 --- a/candidates/step-xxxx-dynamic-tabs-expander-popover.md +++ b/candidates/step-xxxx-dynamic-tabs-expander-popover.md @@ -33,7 +33,25 @@ Add an `on_change`/`rerun` parameter to expanders and tabs that enables lazy exe ## API -### Option 1: Boolean evaluation of container + +Got it πŸ‘ β€” thanks for clarifying. Options **2** and **3** are alternative APIs, not variations of `on_change`. Here’s the corrected API section: + +--- + +## API + +### Option 1: Add `on_change="ignore"|"rerun"|callback` + +This is the same API as for selections on charts/dataframes/maps. It's in fact a similar situation: all of these elements are static and we want to allow making them interactive (i.e. trigger a rerun of the app). This API also allows adding a callback (that is getting executed before the rest of the app). + +```python +exp = st.expander("Show details", on_change="rerun") +tab1, tab2 = st.tabs(["A", "B"], on_change="rerun") +``` + +What makes this complex is the return value: while for chart/dataframe/map selections, we return a dictionary when `on_change` is set, for tabs/expander/popover we need to keep returning the delta generator, so you can add elements to the container. But we need a way to communicate the open/closed state. Some options: + +### Option 1a: Boolean evaluation of container ```python exp = st.expander("Show details", on_change="rerun") @@ -55,18 +73,7 @@ elif tab2: Pros: Clean, readable, minimal changes. Cons: Slight β€œmagic” in truthiness of DGs. -### Option 2: Session state value - -```python -st.tabs(["A", "B"], key="tabs", on_change="rerun") -if st.session_state.tabs == "A": - ... -``` - -Pros: Explicit, consistent with widgets. -Cons: Verbose, less ergonomic for common case. - -### Option 3: DG attribute +### Option 1b: Delta generator attribute ```python exp = st.expander("Expand me", on_change="rerun") @@ -79,15 +86,62 @@ if tab1.open: ... ``` -Pros: Explicit, type-safe, extendable (`.update()` APIs later). +Pros: Explicit, type-safe. Cons: Slightly wordier than Option 1. +### Option 1c: Session state value + +```python +st.tabs(["A", "B"], key="tabs", on_change="rerun") +if st.session_state.tabs == "A": + ... +``` + +Pros: Consistent with widgets. +Cons: Verbose, need to add `key`, need to understand about keys and session state. + + +### Option 2: Function argument + +Pass a function directly; only that function runs when opened/active. + +```python +def show_expander(exp): + exp.write("Heavy content") + +st.expander("Show details", func=show_expander) + +st.tabs({"A": show_tab_a, "B": show_tab_b}) +``` + +- Aligns with lazy-loading patterns, e.g. deferred `st.download_button`. +- We don't really use that pattern anywhere else though; kind of in between widgets/visualizations (which use `on_change`) and fragment/dialog (which use a decorator). +- Could the function automatically be a fragment, similar to `st.dialog`? + +--- + +### Option 3: Function decorator + +```python +@st.expander("Show details") +def show_expander(): + st.write("Heavy content") + +show_expander() +``` + +- Consistent with `@st.fragment` and `@st.dialog` +- But how would this work for `st.tabs`, where we have multiple functions? +- Should expander/tabs/popover content also be a fragment then? Are there any downsides to that? +- Two ways to use `st.expander` etc then – this might be tricky to explain. + + ## Behavior -* When `on_change="rerun"`, interaction triggers rerun. -* Only active container executes its code. -* Inactive containers are skipped, unless already cached or explicitly persisted. +- When made dynamic, opening the expander/popover or switching tabs will trigger a rerun. +- Only the content of the visible container is executed; everything else is skipped. + # Checklist From dadee9c11f72274de1ea3c26c7f81b990d313c5b Mon Sep 17 00:00:00 2001 From: Johannes Rieke Date: Tue, 30 Sep 2025 00:06:19 +0200 Subject: [PATCH 3/4] Remove unneeded stuff --- candidates/step-xxxx-dynamic-tabs-expander-popover.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/candidates/step-xxxx-dynamic-tabs-expander-popover.md b/candidates/step-xxxx-dynamic-tabs-expander-popover.md index cca237b..453e97f 100644 --- a/candidates/step-xxxx-dynamic-tabs-expander-popover.md +++ b/candidates/step-xxxx-dynamic-tabs-expander-popover.md @@ -33,13 +33,6 @@ Add an `on_change`/`rerun` parameter to expanders and tabs that enables lazy exe ## API - -Got it πŸ‘ β€” thanks for clarifying. Options **2** and **3** are alternative APIs, not variations of `on_change`. Here’s the corrected API section: - ---- - -## API - ### Option 1: Add `on_change="ignore"|"rerun"|callback` This is the same API as for selections on charts/dataframes/maps. It's in fact a similar situation: all of these elements are static and we want to allow making them interactive (i.e. trigger a rerun of the app). This API also allows adding a callback (that is getting executed before the rest of the app). @@ -119,7 +112,6 @@ st.tabs({"A": show_tab_a, "B": show_tab_b}) - We don't really use that pattern anywhere else though; kind of in between widgets/visualizations (which use `on_change`) and fragment/dialog (which use a decorator). - Could the function automatically be a fragment, similar to `st.dialog`? ---- ### Option 3: Function decorator From 99b4dc1fa69a33696021c4cccfdcde0be5ed7c13 Mon Sep 17 00:00:00 2001 From: Johannes Rieke Date: Tue, 30 Sep 2025 00:07:44 +0200 Subject: [PATCH 4/4] Move to PR description --- ...step-xxxx-dynamic-tabs-expander-popover.md | 141 +----------------- 1 file changed, 1 insertion(+), 140 deletions(-) diff --git a/candidates/step-xxxx-dynamic-tabs-expander-popover.md b/candidates/step-xxxx-dynamic-tabs-expander-popover.md index 453e97f..396b49e 100644 --- a/candidates/step-xxxx-dynamic-tabs-expander-popover.md +++ b/candidates/step-xxxx-dynamic-tabs-expander-popover.md @@ -1,140 +1 @@ -# Summary - -Make it possible for `st.tabs`, `st.expander`, and `st.popover` content to run lazily (only when opened/active), instead of always executing on every rerun. - -# Problem statement - -Currently, tabs, expanders and popovers always execute their content, even if not visible: - -```python -with st.expander("Show details"): - slow_function() # runs even if expander is closed - -tab1, tab2 = st.tabs(["One", "Two"]) -with tab2: - slow_function() # runs even if Tab 2 is not active -``` - -This ensures instant visibility when toggled, but it slows apps significantly when hidden content is expensive to compute. - -Requests: -- (230 πŸ‘) streamlit/streamlit#6004 -- (93 πŸ‘) streamlit/streamlit#2399 - -Related: -- (79 πŸ‘) streamlit/streamlit#8239 -> should explore in parallel in case there's some implementation overlap to this feature -- (68 πŸ‘) streamlit/streamlit#8265 -> probably separate but should keep an eye on this since it's also about controlling state -- (6 πŸ‘) streamlit/streamlit#6370 -> same, probably separate - - -# Proposed solution - -Add an `on_change`/`rerun` parameter to expanders and tabs that enables lazy execution. - -## API - -### Option 1: Add `on_change="ignore"|"rerun"|callback` - -This is the same API as for selections on charts/dataframes/maps. It's in fact a similar situation: all of these elements are static and we want to allow making them interactive (i.e. trigger a rerun of the app). This API also allows adding a callback (that is getting executed before the rest of the app). - -```python -exp = st.expander("Show details", on_change="rerun") -tab1, tab2 = st.tabs(["A", "B"], on_change="rerun") -``` - -What makes this complex is the return value: while for chart/dataframe/map selections, we return a dictionary when `on_change` is set, for tabs/expander/popover we need to keep returning the delta generator, so you can add elements to the container. But we need a way to communicate the open/closed state. Some options: - -### Option 1a: Boolean evaluation of container - -```python -exp = st.expander("Show details", on_change="rerun") -if exp: # True if open - with exp: - st.write("Expander is open") -``` - -```python -tab1, tab2 = st.tabs(["A", "B"], on_change="rerun") -if tab1: # True if active - with tab1: - st.write("Tab A active") -elif tab2: - with tab2: - st.write("Tab B active") -``` - -Pros: Clean, readable, minimal changes. -Cons: Slight β€œmagic” in truthiness of DGs. - -### Option 1b: Delta generator attribute - -```python -exp = st.expander("Expand me", on_change="rerun") -if exp.open: - with exp: - st.write("Content") - -tab1, tab2 = st.tabs(["A", "B"], on_change="rerun") -if tab1.open: - ... -``` - -Pros: Explicit, type-safe. -Cons: Slightly wordier than Option 1. - - -### Option 1c: Session state value - -```python -st.tabs(["A", "B"], key="tabs", on_change="rerun") -if st.session_state.tabs == "A": - ... -``` - -Pros: Consistent with widgets. -Cons: Verbose, need to add `key`, need to understand about keys and session state. - - -### Option 2: Function argument - -Pass a function directly; only that function runs when opened/active. - -```python -def show_expander(exp): - exp.write("Heavy content") - -st.expander("Show details", func=show_expander) - -st.tabs({"A": show_tab_a, "B": show_tab_b}) -``` - -- Aligns with lazy-loading patterns, e.g. deferred `st.download_button`. -- We don't really use that pattern anywhere else though; kind of in between widgets/visualizations (which use `on_change`) and fragment/dialog (which use a decorator). -- Could the function automatically be a fragment, similar to `st.dialog`? - - -### Option 3: Function decorator - -```python -@st.expander("Show details") -def show_expander(): - st.write("Heavy content") - -show_expander() -``` - -- Consistent with `@st.fragment` and `@st.dialog` -- But how would this work for `st.tabs`, where we have multiple functions? -- Should expander/tabs/popover content also be a fragment then? Are there any downsides to that? -- Two ways to use `st.expander` etc then – this might be tricky to explain. - - -## Behavior - -- When made dynamic, opening the expander/popover or switching tabs will trigger a rerun. -- Only the content of the visible container is executed; everything else is skipped. - - -# Checklist - -TBD \ No newline at end of file +Content is currently in the PR description, will move here later! \ No newline at end of file