lib/streamlit/.agents/skills/developing-with-streamlit/references/ccv2-state-sync.md
This reference shows the canonical CCv2 “controlled component” loop and the most common pitfalls when syncing state between JavaScript and Python.
data, emit via setStateValue)data)default=... (and why it fails)setStateValue(key, value) or setTriggerValue(key, value).component.data and updates the DOM accordingly.This is modeled after Streamlit’s own CCv2 e2e example.
data, emit via setStateValue)Key guideline: only assign to the input when it’s different, or you’ll fight the user’s cursor.
export default function (component) {
const { parentElement, data, setStateValue } = component
const label = parentElement.querySelector("label")
const input = parentElement.querySelector("input")
if (!label || !input) return
label.innerText = data.label
const nextValue = data.value ?? ""
if (input.value !== nextValue) {
input.value = nextValue
}
input.onkeydown = e => {
if (e.key === "Enter") {
setStateValue("value", e.target.value)
}
}
}
data)import streamlit as st
_COMPONENT = st.components.v2.component(
"interactive_text_input",
html="""
<label for="txt">Enter text:</label>
<input id="txt" type="text" />
""",
js=JS, # inline JS string from above
)
def interactive_text_input(*, label: str, initial_value: str, key: str):
# 1) Read current component state from Session State (if it exists)
component_state = st.session_state.get(key, {})
# 2) Compute the value you want the UI to display
value = component_state.get("value", initial_value)
# 3) Send it down to the frontend via `data`
return _COMPONENT(
key=key,
data={"label": label, "value": value},
)
KEY = "my_text_input"
if st.button("Make it say Hello World"):
st.session_state.setdefault(KEY, {})["value"] = "Hello World"
interactive_text_input(label="Enter something", initial_value="Initial Text", key=KEY)
default=... (and why it fails)default={...} is optional. Use it when you want Streamlit to initialize missing state keys for a mounted instance.
Rules:
default must have a corresponding on_<key>_change callback parameter when mounting, or Streamlit raises.Pattern:
result = _COMPONENT(
key=key,
data={"value": value},
default={"value": value},
on_value_change=lambda: None, # required if using default["value"]
)
You’ll see two patterns in the wild:
data.initialX on first mount only. This is useful for initialization but it will not reflect later Python changes.data.value on every render, and only writes when changed.Initial-only example (pitfall for sync):
// If you guard hydration with hasMounted, Python changes won't propagate.
if (typeof data?.initialText !== "undefined" && !hasMountedForKey) {
input.value = String(data.initialText)
}
hasMounted[key] = true
True sync approach (recommended when Python can update the UI):
const nextValue = data.value ?? ""
if (input.value !== nextValue) input.value = nextValue
Streamlit may raise if you modify st.session_state.<key>.<field> after the component with that key has been instantiated in the same run.
Safe patterns:
st.session_state[key][...] before mounting the component (e.g., in a button handler placed above the mount call).input.value when it differs from the data value.data every run; avoid initial-only hydration guards if you want true sync.default raises: ensure every default key has a corresponding on_<key>_change callback parameter.st.session_state[key][...] = ... earlier in the script (before mount), or restructure into a two-run flow (set state then rerun).