I’ve solved the requirements:
- Dynamic filtering: Products filter updates based on the selected category
- Batch application: Changes only apply to the main display after clicking “Apply”
The Challenge
In my original post, I was struggling to combine these two features because:
- Using
st.form
prevents dynamic updates between form elements
- Without forms, any widget interaction triggers a full app rerun
Solution Overview
My solution uses session state to maintain two separate filter states:
- Temporary filters: What the user is currently selecting (updates in real-time)
- Applied filters: What’s actually affecting the main content (updates only on “Apply”)
- st.fragment(): Isolates reruns to certain sections of code using a function decorator
This approach gives us the best of both worlds - immediate dropdown updates with deferred application to the main content.
Key Components of the Solution
1. Dual Session State
# Initialize session state
def initialize_filters():
if 'applied_filters' not in st.session_state:
st.session_state.applied_filters = {
'category': list(product_data.keys())[0],
'product': product_data[list(product_data.keys())[0]][0]
}
if 'temp_filters' not in st.session_state:
st.session_state.temp_filters = {
'category': list(product_data.keys())[0],
'product': product_data[list(product_data.keys())[0]][0]
}
2. Apply filters
def apply_filters():
"""Copy temp filters to applied filters"""
st.session_state.applied_filters = st.session_state.temp_filters.copy()
3. st.fragment()
@st.fragment
def filters_fragment():
"""Fragment containing filter widgets"""
# Product Category Filter
category = st.selectbox(
'Select Product Category',
options=list(product_data.keys()),
key="category_filter",
index=list(product_data.keys()).index(st.session_state.temp_filters['category'])
if st.session_state.temp_filters['category'] in product_data.keys() else 0
)
# Update temp filters with current category
if "category_filter" in st.session_state:
st.session_state.temp_filters['category'] = st.session_state.category_filter
# Product Filter - options depend on selected category
products = product_data.get(st.session_state.temp_filters['category'], [])
product = st.selectbox(
'Select Product',
options=products,
key="product_filter",
index=products.index(st.session_state.temp_filters['product'])
if st.session_state.temp_filters['product'] in products else 0
)
# Update temp filters with current product
if "product_filter" in st.session_state:
st.session_state.temp_filters['product'] = st.session_state.product_filter
# Display filter status
st.markdown("---")
if st.session_state.temp_filters != st.session_state.applied_filters:
st.info("You have unapplied filter changes")
else:
st.success("Filters are applied")
4. Main Content Using Applied Filters Only
def main():
st.title("Product Filter App")
# Initialize session state
initialize_filters()
# Sidebar with filters fragment
with st.sidebar:
st.header("Filters")
# Call the fragment function inside the sidebar
filters_fragment()
# Apply filters button
if st.button("Apply Filters", key="apply_filters"):
apply_filters()
# Main content area:
st.header("Selected Products")
# Display content based on applied filters (not temp filters)
applied_category = st.session_state.applied_filters['category']
applied_product = st.session_state.applied_filters['product']
st.subheader("Applied Filters")
st.write(f"Category: {applied_category}")
st.write(f"Product: {applied_product}")
# Display a sample visualization based on applied filters
st.subheader("Product Details")
st.info(f"Showing detailed information for {applied_product} in {applied_category} category")
# Demo metrics
metrics1, metrics2, metrics3 = st.columns(3)
with metrics1:
st.metric("Price", "$299" if applied_category == "Electronics" else "$49")
with metrics2:
st.metric("Rating", "4.8/5.0" if applied_product == "Smartphone" else "4.2/5.0")
with metrics3:
st.metric("Stock", "In Stock")
# Show both temp and applied filters for demonstration
with st.expander("Debug - Filter States"):
st.write("Temporary Filters:", st.session_state.temp_filters)
st.write("Applied Filters:", st.session_state.applied_filters)
if __name__ == "__main__":
product_data = {
'Electronics': ['Laptop', 'Smartphone', 'Tablet'],
'Clothing': ['Shirt', 'Pants', 'Jacket'],
'Groceries': ['Fruits', 'Vegetables', 'Dairy']
}
main()
Why This Approach Works
- Real-time dropdown updates: When the category changes, the product dropdown immediately updates because the fragment only reruns the filter widgets, not the whole app.
- Deferred application: Main content only updates when the “Apply” button is clicked, copying from temp_filters to applied_filters.
Why Fragments Matter Here
Fragments are the perfect solution for this use case because:
- They allow isolated reruns of just the filtering section when category selection changes, avoiding full app reruns.
- They maintain state continuity - the fragment keeps track of its internal state using st.session_state between reruns.
- They provide clear separation of concerns - filtering logic stays contained in one function.
- They offer flexible placement - the fragment can be used in the sidebar or anywhere else in the app without changing its internal logic. Just be aware you should not use the st.sidebar.input_widget() syntax when wrapping the function the the decorator or you’ll get an error: Fragments cannot write widgets to outside containers.
Final Thoughts
This pattern can be extended to handle more complex filtering scenarios, such as:
- Multiple interdependent filter groups
- Filters with different types (dropdowns, sliders, checkboxes)
- Filter presets or saved configurations
Hope this is useful for anyone struggling with this problem too. 