streamlit / streamlit

Streamlit — A faster way to build and share data apps.
https://streamlit.io
Apache License 2.0
36k stars 3.11k forks source link

Strange behavior when synchronizing st.session_state to localStorage #5102

Closed sg-s closed 2 years ago

sg-s commented 2 years ago

Summary

Apologies if this is out of scope and this is an issue with streamlit_javascript. What i'm trying to do is use a persistent state stored in localStorage that persists across sessions.

Steps to reproduce

This is my minimum working code that demonstrates the problem:

from time import sleep

import streamlit as st
from streamlit_javascript import st_javascript

# no matter what, update the session state from localStorage
# if there is nothing in localStorge,
# then it is set to 0 (which makes sense)
# if there is something, then we set it to that value
st.session_state.current_streak = int(st_javascript("localStorage.current_streak;"))

# logic

def common_callback(idx):
    if idx == "+":
        st.session_state.current_streak += 1

    else:
        st.session_state.current_streak -= 1

    st_javascript(f"localStorage.current_streak = {st.session_state.current_streak};")

    # removing this sleep breaks everything
    sleep(0.1)

# layout
st.metric("Current streak", st.session_state.current_streak)

st.button("+", on_click=common_callback, args=("+",))
st.button("-", on_click=common_callback, args=("-",))

Note that removing the sleep(.1) breaks everything, which is the basis of my bug report. I believe this should not be the case.

Expected behavior:

it should work if i remove the sleep

Actual behavior:

doesn't work -- doesn't update the localStorage, which tells me that it isn't being run at all.

vdonato commented 2 years ago

Hi @sg-s,

I think isn't quite an issue with either streamlit or streamlit_javascript. What's probably happening here is that, without the sleep, the two st_javascript commands run so quickly that the second call to st_javascript is the one that actually gets passed to the component as a prop. In this case, that's the st_javascript("localStorage.current_streak;") line (since callbacks are run before the main script run). We can't do too much to prevent this given that st_javascript is implemented as a custom component, so it's technically a react component (even though there's nothing visually displayed), which means that react may decide to only "render" the component once with its most recent props.

This could probably be fixed on the st_javascript side by teaching the component to save each prop passed to it in some form of state to ensure that no commands ever get dropped, but doing so would be up to the component author.

Alternatively, changing your code to initialize st.session_state.current_streak by doing something like

if 'current_streak' not in st.session_state:
    st.session_state.current_streak = int(st_javascript("localStorage.current_streak;"))

would prevent the calls to st_javascript used to initialize st.session_state.current_streak in the main script from overwriting the the calls to update the value in localStorage.