Streamlit multiselect reselecting items

good day.

The problem occurs in a local Streamlit application that allows users to select documents for analysis. The main issue is that when a user deselects a document, it gets reselected automatically. This unexpected behavior only resolves after deselecting the same document a second time.

Specifically:

  1. The application presents a list of documents to the user via a Streamlit multiselect widget.
  2. When the user attempts to deselect a document for the first time, the deselection doesn’t take effect immediately.
  3. The deselected document reappears in the selection, as if it was never deselected.
  4. Only when the user tries to deselect the same document again does the deselection actually work.

This behavior is likely caused by an interaction between Streamlit’s state management and the custom logic implemented to handle document selection. The issue might be related to how the application is updating and storing the list of selected documents, possibly involving conflicts between the Streamlit session state and the multiselect widget’s internal state.

The problem persists despite attempts to modify the code, suggesting that there might be a more fundamental issue with how the application is managing state or how it’s interacting with Streamlit’s rerun behavior

        document_options = [f"{i} : {doc.split("\\")[-1]} : {col}" for i, (col, doc) in enumerate(all_documents)]

        #st.write(f"Available Documents: {document_options}")

        if 'selected_docs' not in st.session_state or st.session_state.selected_docs == []:
            st.session_state.selected_docs = document_options
        
        if 'deselected_docs' not in st.session_state or st.session_state.deselected_docs == []:
            st.session_state.deselected_docs = []
        

        if 'contador' not in st.session_state:
            st.session_state.contador = 0
        #document_options = [f"{col}:{i}:{doc}" for i, (col, doc) in enumerate(all_documents)]
        if st.button("invertir"):
            st.session_state.selected_docs, st.session_state.deselected_docs = st.session_state.deselected_docs, st.session_state.selected_docs

        selected_docs = st.multiselect(
            "Choose documents to include in the analysis:",
            options=document_options,
            default=st.session_state.selected_docs
        )
        
        st.write(f"Selected Documents: {selected_docs}")
        st.write(f"Selected Documents: {st.session_state.selected_docs}")

        

        st.session_state.contador += 1

        st.write(f"contador: {st.session_state.contador}")
        st.write(f"Selected Documents: {st.session_state.selected_docs}")
        filtered_docs = []
        not_selected = []
        not_selected_format=[]
        filtered_docs_format = []
        for doc in all_documents:
            for selected_doc in selected_docs:
                #st.write(f"doc: {selected_doc}")
                doc_name = doc[1].split('\\')[-1].strip().lower()
                selected_doc_name = selected_doc.split(':', 2)[1].strip().lower()

                #st.write(f"documento: {doc_name}")
                #st.write(f"selected_doc: {selected_doc_name}")

                if doc_name == selected_doc_name:
                    filtered_docs.append(doc[1])

            
            if doc[1] not in filtered_docs:
                not_selected.append(doc[1])
        # Agregar a 'test' los selected_docs que no se emparejen con ningún documento en filtered_docs
        #st.write(f"Selected Documents: {filtered_docs}")
        #st.write(f"Selected Documents: {selected_docs}")
        #st.write(f"not Selected Documents: {not_selected}")
        #st.write(f"Documents not selected: {not_selected}")
        # Crear un diccionario que mapee las rutas completas a sus índices y nombres cortos
        path_to_info = {doc: (i, doc.split("\\")[-1], author) for i, (author, doc) in enumerate(all_documents)}

        
        for doc in not_selected:
            if doc in path_to_info:
                i, short_name, author = path_to_info[doc]
                not_selected_format.append(f"{i} : {short_name} : {author}")

        for doc in filtered_docs:
            if doc in path_to_info:
                i, short_name, author = path_to_info[doc]
                filtered_docs_format.append(f"{i} : {short_name} : {author}")

        
        #st.write(f"Selected Documents: {filtered_docs}")

        
        st.session_state.selected_docs, st.session_state.deselected_docs = filtered_docs_format, not_selected_format

        #st.write(f"Selected Documents: {st.session_state.selected_docs}")
                 
        #st.write(f"Filtered Documents: {filtered_docs}")
        sd = st.session_state.selected_documents = filtered_docs
        selected_docs = st.multiselect(
            "Choose documents to include in the analysis:",
            options=document_options,
            default=st.session_state.selected_docs
        )

This is an anti-pattern. When the value in st.session_state.selected_docs changes, the widget resets. This creates a “double submit” problem. Rather than passing a default value, try setting key="selected_docs" to directly associate the widget with a value in Session State. (Then you don’t need to manually recompute it later.)