Welcome to the Streamlit community, and thanks for sharing your code and a clear description of your challenge!
You’re right: preserving edits and selections across dynamic filters in Streamlit is tricky, especially with large data and when using st.data_editor and custom filters. The main issue is that when you filter, the edited DataFrame only contains the visible rows, so saving it directly will overwrite or “delete” filtered-out rows. Also, putting on_change on the data editor leads to the “double edit” problem due to Streamlit’s rerun model.
Best Practice:
To persist edits and selections across filters, you should:
- Store the full, unfiltered DataFrame in
st.session_state. - When the user edits the filtered view, update only the corresponding rows in the full DataFrame.
- Avoid using
on_changeon the data editor; instead, use a callback or button to commit changes, or update the session state immediately after editing.
Here’s a minimal, reproducible example that demonstrates this pattern:
import streamlit as st
import pandas as pd
import numpy as np
from streamlit_dynamic_filters import DynamicFilters
num_rows = 500
np.random.seed(42)
if 'df1' not in st.session_state:
data = []
for i in range(num_rows):
data.append({
"Preview": f"https://picsum.photos/400/200?lock={i}",
"Views": np.random.randint(0, 1000),
"Active": np.random.choice([True, False]),
"Category": np.random.choice(["🤖 LLM", "📊 Data", "⚙️ Tool"]),
"Progress": np.random.randint(1, 100),
})
st.session_state.df1 = pd.DataFrame(data)
df1 = st.session_state.df1
dynamic_filters = DynamicFilters(df1, filters=['Category', 'Active'])
with st.sidebar:
dynamic_filters.display_filters()
progress = st.slider("Progress", 1, 100, 100)
filtered_df = dynamic_filters.filter_df()
filtered_df = filtered_df[filtered_df['Progress'] < progress]
# Show editable table
edited_df = st.data_editor(filtered_df, hide_index=True, use_container_width=True, num_rows="dynamic", key="editor")
# Commit edits back to the full DataFrame
if st.button("Save changes"):
for idx, row in edited_df.iterrows():
# Find the corresponding index in the original df
orig_idx = df1.index[df1['Preview'] == row['Preview']]
if not orig_idx.empty:
st.session_state.df1.loc[orig_idx, :] = row
st.write("Full DataFrame (persisted):")
st.write(st.session_state.df1.head())
Key Points:
- Edits are only committed to the full DataFrame when the user clicks “Save changes”.
- The original DataFrame in session state is never overwritten by a filtered subset.
- This avoids the “double edit” bug and preserves edits across filter changes.
For more advanced UX, you could use a custom callback or experiment with Session State patterns to auto-save, but a button is the most robust and user-friendly approach for now.
Sources: