St.button behavior and handlers

:slight_smile: Hi All, especially Streamlit developers (if they are reading this)!

Once again about bugs & errors in st.button handlers.

Recently, the documentation for the st.button has been significantly updated and expanded, and examples of use, tips, etc. have appeared. This is very good, but errors in the handlers, unfortunately, have not been corrected, and it is not so good…

To demonstrate errors in handlers I will give an example of shortcode (streamlit==1.31.1, Python 3.11)

import streamlit as st

if 'run_counter' not in st.session_state:
    st.session_state['run_counter'] = 1
if 'is_add_disabled' not in st.session_state:
    st.session_state['is_add_disabled'] = False
if 'is_remove_disabled' not in st.session_state:
    st.session_state['is_remove_disabled'] = False

st.write("'run_counter' = ", st.session_state['run_counter'])

def add_on_click(param1, param2):
    st.write("Call add_on_click(param1, param2)...")
    st.write(param1, param2)
    st.session_state['is_remove_disabled'] = not st.session_state['is_remove_disabled']

def remove_on_click(param3, param4):
    st.write("Call remove_on_click(param3, param4)...")
    st.write(param3, param4)
    st.session_state['is_add_disabled'] = not st.session_state['is_add_disabled']

def reset_on_click(param5, param6):
    st.write("Call remove_on_click(param5, param6)...")
    st.write(param5, param6)
    st.session_state['is_add_disabled'] = False
    st.session_state['is_remove_disabled'] = False

row = st.columns(3)

with row[0].container(border=True):
    if st.session_state['run_counter'] % 3 == 0:
        st.button(label="Add", on_click=add_on_click, key="add_button_key",
                  kwargs={"param1": "value1", "param2": "value2"}, disabled=st.session_state.is_add_disabled,)

with row[1].container(border=True):
    st.button(label="Remove", on_click=remove_on_click, key="remove_button_key",
          kwargs={"param3": "value3", "param4": "value4"}, disabled=st.session_state.is_remove_disabled,)

with row[2].container(border=True):
    st.button(label="Reset", on_click=reset_on_click, key="reset_button_key",
          kwargs={"param5": "value5", "param6": "value6"}, disabled=False,)

if "add_button_key" in st.session_state:
    st.write("add_button_key: ", st.session_state["add_button_key"])

if "remove_button_key" in st.session_state:
    st.write("remove_button_key: ", st.session_state["remove_button_key"])

st.session_state['run_counter'] += 1

This is redundant code as it was created to model more than just the st.button behavior, please ignore that. So the code creates 3 buttons, with the first (“Add button”) being created every third execution of the main loop. The callback functions on the first (“Add”) and second (“Remove”) buttons swap the “disable status” opposite one. The third button (“Reset”) makes the first and second buttons available.
There is also a launch counter inside, its value is displayed on the screen constantly.

Let’s execute the app! Now press the second button (“Remove”) 8 times, step by step. We see the following Streamlit states:

Now let’s analyze the presence of the “Add” button key value in the st.session_state:

STEP # Run_Counter % 3 Is Add Button Displayed if “add_button_key” in st.session_state (at end the main loop)
1 - - -
2 - - -
3 + + +
4 - - +
5 - - -
6 + + +
7 - - +
8 - - -

So, first step:
The button is not created due to “%3” and there is no key in the st.session_state. - Correct!
Second step:
The button is not created, there is still no key in the st.session_state. - Correct!
Third step:
The button has been created, the key is in the st.session_state. - Correct!
Fourth step:
The button is not created, but the key is still in the st.session_state. - Incorrect!!!
Fifth step:
The button is not created and the kernel finally realized it (with a one-cycle delay!) - Correct!
… and so on.

It can be seen that steps 4, 7, 10, … 1+3*n have an incorrect state of st.button key in the st.session_state.

I would like to hear the developers’ comments. Thanks!