Streamlit login solution need to click on "Login" button twice to login

Login button need to be clicked twice. Ideally single click should work.

import streamlit as st


# Function to check login credentials
def login(username, password):
    return username == "a" and password == "b"

# Main function
def main():
    st.title("abc")

# Get session state
if "login_successful" not in st.session_state:
    st.session_state.login_successful = False
if "username" not in st.session_state:
    st.session_state.username = ""
if "password" not in st.session_state:
    st.session_state.password = ""

# If login is successful, display "Hello"
if st.session_state.login_successful:
    st.write("Hello")
else:
    # Display login form
    st.session_state.username = st.text_input("Username:", value=st.session_state.username)
    st.session_state.password = st.text_input("Password:", type="password", value=st.session_state.password)

    # Check login credentials
    if st.button("Login"):
        if login(st.session_state.username, st.session_state.password):
            # Update session state on successful login
            st.session_state.login_successful = True
        else:
            st.warning("Wrong username or password.")

if __name__ == "__main__":
    main()

Hi,

Streamlit reruns the entire code from the top on any widget interaction, except in a form where the rerun trigger is the form’s submit button.

If you want updated state to be immediately available in the rerun, then you must use on_change or on_click callbacks. Callbacks are executed before the rerun, so they are a great place to assign session state variables required during the scheduled (upcoming) rerun.

In your case, the login button click invokes a rerun and when the button is evaluated during the rerun its state is true so the login state will be checked and updated to true if the username and password are correct. Then, rerun ends.

The updated login state will not be visible to the code above the button until the next rerun, i.e. when you click the button again. Hence, two clicks are required.

To fix this problem, you can force a rerun (using st.rerun) after the if/else in the button’s if block. If a rerun is not desirable, then use callbacks. But reruns can cause widget values to be reset, so you likely need to use callbacks anyway to make your apps behave properly!

A word of caution: Callbacks solve most state problems in Streamlit apps, but in apps with many widgets (i.e., large apps) you can end up with what I call callback pollution – many tiny callbacks, attached to your many widgets, across many pages. In my apps I implement a generalized callback handler which can be used for the majority of my callback cases where I simply need to set state values from the underlying widget values. (You will need a key on each input widget, so that the widget values can be obtained and assigned to the corresponding session state variables. Adopt a sensible naming convention for widget keys.)

Below is your example rewritten in my idiomatic style, which you may or may not like, but my experience is that it works very well. One benefit of using callbacks, and the way I lay it out, is that I can keep all state management in one place. I don’t have state assignments all over my code, even directly from widgets to state variables! Using this generic callback I can even set multiple state values based on a single widget interaction.

import streamlit as st

# State management -----------------------------------------------------------

# I like to use state instead of the long form 
state = st.session_state

def init_state(key, value):
  if key not in state:
    state[key] = value

# generic callback to set state
def _set_state_cb(**kwargs):
    for state_key, widget_key in kwargs.items():
        val = state.get(widget_key, None)
        if val is not None or val == "":
            setattr(state, state_key, state[widget_key])

def _set_login_cb(username, password):
    state.login_successful = login(username, password)  

def _reset_login_cb():
    state.login_successful = False
    state.username = ""
    state.password = "" 

init_state('login_successful', False)
init_state('username', '')
init_state('password', '')

# -----------------------------------------------------------------------------

# Function to check login credentials
def login(username, password):
    return username == "a" and password == "b"

# Main function
def main():
    st.title("My App")

    # If login is successful, display "Hello"
    if state.login_successful:
        st.subheader("My Page")
        st.write("Hello")
        st.button("Logout", on_click=_reset_login_cb)
    else:
        st.subheader("Login")
        # Display login form
        st.text_input(
            "Username:", value=state.username, key='username_input',
            on_change=_set_state_cb, kwargs={'username': 'username_input'}
        )
        st.text_input(
            "Password:", type="password", value=state.password, key='password_input',
            on_change=_set_state_cb, kwargs={'password': 'password_input'}
        )

        # st.write(state.username)
        # st.write(state.password)
        
        # Check login credentials
        if not state.login_successful and st.button("Login", on_click=_set_login_cb, args=(state.username, state.password)):
            st.warning("Wrong username or password.")

if __name__ == "__main__":
    main()

Requesting your help?
Your problem is encountered often, and in my opinion is one of the main issues why users tend to struggle with (and even give up) on Streamlit. Did you read the tutorials in the docs first? Was it hard to find the needed info? How can the guidance be improved? Please let the product team or any of the official Streamlit folks in this forum know where things in this area can be improved. Thanks.

For further details on state, look at:

P.S. I’m just a regular user like you.

Happy Streamlitin’
Arvindra

1 Like

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