Form submit button callback is "one click behind"

I have a form that when I click the submit button I’d like a DB update to take place. I am using the session state to store the values needed for the DB update. Then in the callback I am accessing the session state to build and execute the update statement:

import streamlit as st;
def updateNotes():
    notesUpdateSQL="UPDATE NOTES_TABLE SET NOTES=?,UPDATEDT=CURRENT_TIMESTAMP() WHERE USERID=?";
    notesUpdateSQLArgs=[st.session_state.updatedNotes,st.session_state.currentUserId];
    # Execute db operation here.

    #Write args to see what would be sent.
    #Always one click behind?
    st.write(notesUpdateSQLArgs);
    st.success("Notes Updated");
    
with st.form("notesForm"):
    taNotes=st.text_area("Notes", value="");
    currentUserId="123456"; #Simulated user ID
    st.session_state.updatedNotes=taNotes;
    st.session_state.currentUserId=currentUserId;
    st.form_submit_button("Update Notes",on_click=updateNotes);

But I have noticed it is always “one click behind” what is in the text_area. I don’t know how else to put it.

For example, on a fresh run of the app, I type in “AAA” and see this after clicking:

Then the next thing I do is type in “BBB” and I see this after clicking:

Its like I am only capturing what “used” to be in the text_area, rather than what I just manually updated it to be using the keyboard.

Can anyone tell me what I am doing wrong?

Thank you.

Don’t use a form for that, it just adds unnecessary complication.

How else should I be doing it? It seems like a form is exactly what I need.

Just get rid of the form, use a standard button instead of a form_submit_button.

Ok that seems to work in this instance, but I might have a use case in the future where I need multiple different inputs to be entered for a db operation. To prevent the app from rerunning after entering the first input, I would need to use a form.

Can you tell me if it is possible to do this with a form?

Don’t use a callback then. By the time the callback runs, the submitted values are not yet in session_state. I think this should work:

with st.form("notesForm"):
    taNotes=st.text_area("Notes", value="")
    currentUserId="123456"; #Simulated user ID
    st.session_state.updatedNotes=taNotes
    st.session_state.currentUserId=currentUserId
    submitted = st.form_submit_button("Update Notes")
if submitted:
    updateNotes()

You can do that in a form using keys and accessing them through the session state in the callback like this (the id can be passed as an argument since it isn’t defined by the user in an input):

import streamlit as st;
def updateNotes(UserId: int):
    # get the session state here
    updatedNotes = st.session_state.updatedNotes

    notesUpdateSQL="UPDATE NOTES_TABLE SET NOTES=?,UPDATEDT=CURRENT_TIMESTAMP() WHERE USERID=?";
    notesUpdateSQLArgs=[updatedNotes, UserId];
    # Execute db operation here.

    #Write args to see what would be sent.
    #Always one click behind?
    st.write(notesUpdateSQLArgs);
    st.success("Notes Updated");
    
with st.form("notesForm"):
    updatedNotes=st.text_area("Notes", value="", key=updatedNotes);
    currentUserId="123456"; #Simulated user ID
    st.form_submit_button("Update Notes", on_click=updateNotes, args=(currentUserId, ));
1 Like

To explain why this doesn’t work for the sake of edification:

  1. The script runs. toNotes has the default value of your text area. You then assign that default value to st.session_state.updatedNotes.
  2. The user updates the text area. The script doesn’t rerun. The output is still the default value.
  3. The user clicks the submit button.
    • The callback executes first. The value in st.session_state.updatedNotes is still the default value.
    • The app reruns.
    • Within the form, the widget now has the new output.
    • st.session_state.updatedNotes now has the new value (after the callback is already done).

The key point in @msquaredds’ answer is that a key is assigned to the widget within the form. When you click the submit button of a form, all the values of the widgets in Session State are updated first, then the callback runs, then the script runs which updates their output. You were one step behind because you were relying on the widget output to store the value in Session State instead of communicating with the widget directly through its key.

1 Like

Thank you for this example, I really appreciate it! Handles my use case.

Thank you for the explanation. I am coming from the world of Java Swing into Streamlit. I am used to being able to get the updated value of a UI component after it has been interacted with while the program is running.

I guess what I need to remember is that the Streamlit app is not running when you are looking at it. Its really just a webpage, and things only “happen” during the rerun.

1 Like

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.