Execute button callback once

Summary

Right now we allow users to customise some data, and then hit a button to upload this data into an S3 bucket. Right now, we’re trying to figure out how to ensure this upload happens once, but right now it seems if a user double clicks this button then the streamlit code will run twice.

We’ve tried using st.session_state, global is_uploading variables, and even a threading Lock to try and ensure that this upload can only happen once. I’m sure there must be an easy solution I’ve overlooked, and so here I am, requesting some help in doing this!

Steps to reproduce

import time

import streamlit as st


def dummy_upload():
    print("Uploading!")
    time.sleep(1)
    print("Upload done!")


if __name__ == "__main__":
    if st.button("Upload now"):
        dummy_upload()

Expected behavior:

A pattern whereby we only trigger the upload once (aka the dummy_upload is only called once, or theres a way to return out of it if something else is already executing).

Actual behavior:

A double click causes a double upload.

Debug info

  • Streamlit version: 1.25.0
  • Python version: 3.11.4
  • pyenv
  • OS version: Ubuntu
  • Browser version: All

Requirements file

Using Conda? PipEnv? PyEnv? Pex? Share the contents of your requirements file here.
Not sure what a requirements file is? Check out this doc and add a requirements file to your app.

Links

  • Link to your GitHub repo:
  • Link to your deployed app:

Additional information

If needed, add any other context about the problem here.

Hi there!

I’m not sure about other options, but I think st.session_state should work for this:

import time

import streamlit as st

if 'button_clicked' not in st.session_state:
    st.session_state['button_clicked']=False
def dummy_upload():
    st.write("Uploading!")
    time.sleep(1)
    st.write("Upload done!")


if __name__ == "__main__":
    mybutton = st.button("Upload now")
    if not st.session_state['button_clicked']:
        if mybutton:
            dummy_upload()
            st.session_state['button_clicked']=True
    else:
        st.write('Clicked the button already')

I might be missing something, but I believe this is the behaviour you are looking for. You can click the button only once, and after clicking the button, the button_clicked variable in the session state changes to True, which can be used in a simple if-else to avoid multiple clicks.

Please note that this will persist throughout the user session (until page refresh or app restart), so in your case, if the user modifies the data again, you will need to set the ‘button_clicked’ variable back to False in your modify() function to ensure the button can be clicked again when the user updates data.

Cheers,
Moiz

Hey Moiz! I think the st.session_state gets us most of the way there, but I’m having trouble understanding how it functions (the initial reason I thought it was not applicable).

Take the following code:

import time

import streamlit as st


def dummy_upload():
    key = "can_upload"
    if st.session_state.get(key, True):
        st.session_state[key] = False
        print("Uploading!")
        time.sleep(2)
        st.session_state[key] = True
        print("Upload done!")


if __name__ == "__main__":
    with st.form("uploader"):
        submit = st.form_submit_button("Upload now")
    if submit:
        dummy_upload()
        print(st.session_state)

If I click, wait for the “Upload Done!” message, and then click again, it works great. If I double click, it stops the second flick from doing the fake upload. But for some reason I’m unsure about, if the user double clicks, it resets the session state.

Here’s the output from the prints if I do a “Click, wait enough, Click, wait enough, spam click”

Uploading!
Upload done!
{'can_upload': True, 'FormSubmitter:uploader-Upload now': True}
Uploading!
Upload done!
{'can_upload': True, 'FormSubmitter:uploader-Upload now': True}
Uploading!
{'can_upload': False, 'FormSubmitter:uploader-Upload now': True}
{'can_upload': False, 'FormSubmitter:uploader-Upload now': True}
{'can_upload': False, 'FormSubmitter:uploader-Upload now': True}
{'can_upload': False, 'FormSubmitter:uploader-Upload now': True}
Upload done!
{}
{'can_upload': False, 'FormSubmitter:uploader-Upload now': True}

For some reason the session state can_upload state is not being reset to true, as if the second click causes a copy of the state and then the original “Set it back to true” line has no effect.

Any ideas how to get around this?

My solution I hacked together to solve a similar problem:

I had to attach a volume disk so I could write out files.

At the beginning of the script, when the page first loads, I assign the person a random ID:

import string
import random

if "random_id" not in st.session_state:
    FILE_LEN = 32
    st.session_state["random_id"] = ''.join(random.choices(string.ascii_uppercase +string.digits, k=FILE_LEN))    

Then, when I am about to send off my API call I check the following:

def need_to_send_api(some_id):
    try:
        f = open(f"{st.session_state['random_id']}.txt", "r")
        f.close()
        return False
    except:
        file = open(f"{st.session_state['random_id']}.txt", "w")
        file.writelines(some_id)
        file.close()
        return True

send_api = need_to_send_api(some_id)

and if send_api is True, I make the API call. some_id is just some other unique ID i choose to write into the file. this should work with an empty file though. writing or creating a small file is so fast that no double click would be faster - also, Python locks accessing the file to a single thread at a time I believe - but dont quote me on that.

This is obviously imperfect, but it works. I also have a method that will remove any ID.txt files that are older than an hour to avoid worrying about space. One other downside is that as this is executing, you can still double click the button and if so the whole process takes even longer - but still only 1 API call is ever made (as far as my testing would indicate)

I am ready for someone to tell me this was stupid, but I would love to hear a better way.