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 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:
- 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). - 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
- 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(...)