Saving data in local storage via streamlit

Due to the current runtime model, streamlit components cannot return value immediately when they are created. So, st_js instantiates first, and then reruns streamlit app (when value is returned from js). So, the major limitation is that st_js should be accessible two times with the same key – 1. load component, 2. retrieve it’s return value.

st_js_blocking just stops after the first run in order to wait until js code is finished and triggers page rerun.

So, since every st_js/st_js_blocking call should be loaded twice, the code will look a bit silly. On the top of that, the code can get the value before it actually set it (if the set is between its 1st and 2nd run and your get operation is above in your set code 🫨). As you see, there are lot of limitations, and I did not come up with the idea of how to better reflect them in my component design :unamused: If you have any ideas, they are highly appreciated.

Here is an updated code snippet. Still limited, but does what it should + its more or less clear.

Main changes:

  1. Generate component key on the first run of getitem. The latter calls with the same arg will use the same instance of the component (and it won’t call js again).
  2. Clear component key on changes (set/delete). So, getting the same local storage key will instantiate new component with new component key, but with the same js code
  3. Updated main in order to reflect the reloading nature of the runtime model. Also, saved key in local storage to better reflect the idea - on page reload you see the data immediately
import json
import uuid
from typing import Any

import streamlit as st
from streamlit_js import st_js, st_js_blocking


class StLocalStorage:
    """An Dict-like wrapper around browser local storage.

    Values are stored JSON encoded."""

    def __init__(self, prefix: str = "") -> None:
        self._keys = {}
        self._prefix = prefix

    def __getitem__(self, key: str) -> Any:
        code = f"""
        return JSON.parse(localStorage.getItem('{self._prefix + key}'));
        """
        id = f"get_{key}"
        if id not in self._keys:
            self._keys[id] = str(uuid.uuid4())

        result = st_js_blocking(code, key=self._keys[id])
        return json.loads(result) if result else None

    def __setitem__(self, key: str, value: Any) -> None:
        print("set", key, value)
        value = json.dumps(value, ensure_ascii=False)
        code = (
            f"localStorage.setItem('{self._prefix + key}', JSON.stringify('{value}'));"
        )
        st_js(code)


        # getitem has to refresh the value
        get_id = f"get_{key}"
        if get_id in self._keys:
            del self._keys[get_id]

    def __delitem__(self, key: str) -> None:
        code = f"localStorage.removeItem('{self._prefix + key}');"
        st_js_blocking(code)

        # getitem has to refresh the value
        get_id = f"get_{key}"
        if get_id in self._keys:
            del self._keys[get_id]


    def __contains__(self, key: str) -> bool:
        val = self.__getitem__(key)
        return bool(val)


ss = st.session_state
if __name__ == "__main__":

    st.title("st_local_storage basic example")
    "Any values you save will be available after leaving / refreshing the tab"

    if "ls" not in ss:
        ss.ls = StLocalStorage()

    if "key" not in ss:
        ss.key = ss.ls["key"]
        ss._key = ss.key

    st.text_input("Key", key="_key")
    if ss._key != ss.key:
        # Order matters here, the condition should remain the same until js code finishes
        ss.ls["key"] = ss._key
        ss.key = ss._key

        # if the key is changed, the value should be refreshed
        if "value" in ss:
            del ss["value"]


    if not ss.key:
        st.stop()

    # Same shit as before, but ss.key instead of "key"
    if f"value" not in ss:
        ss.value = ss.ls[ss.key]
        ss._value = ss.value

    st.text_input("Value", key="_value")
    if ss._value != ss.value:
        ss.ls[ss.key] = ss._value
        ss.value = ss._value

One more limitation is that any streamlit component takes some space on the page. So, if we run js code, on the first run it shows blank lines. It can be hacked by using my st_invisible_container

with st_invisible_container():
    st_js(...)
1 Like

@jcarroll, I saw the PR you created, so, you might be interested in this little update

Consider each call as a different child at home.

Giving the same name to each child will create chaos in the house.

The key works in Streamlit the same way.

Just try to pass a different key each time you call the function, and you can easily eliminate this error.