Stores
Stores¶
Stores provide reactive state management for nested objects and lists. Inspired by SolidJS createStore, they let you work with complex state while maintaining fine-grained reactivity — each property path is tracked independently.
Creating a store¶
from wybthon import create_store
store, set_store = create_store({
"count": 0,
"user": {"name": "Ada", "age": 30},
"todos": [
{"id": 1, "text": "Learn Wybthon", "done": False},
],
})
create_store returns a (store, set_store) tuple, similar to create_signal.
Reading values¶
Access store values via attribute syntax. Reads are reactive — any effect or render function that reads a store property will re-run when that specific property changes:
store.count # 0
store.user.name # "Ada"
store.todos[0].text # "Learn Wybthon"
Nested dicts become store proxies and list values become list proxies, so reactivity extends to any depth.
Writing values¶
Use the set_store setter with a path-based API:
# Simple set
set_store("count", 5)
# Functional update
set_store("count", lambda c: c + 1)
# Nested path
set_store("user", "name", "Jane")
# Path through list index
set_store("todos", 0, "done", True)
# Replace an entire nested object
set_store("user", {"name": "Grace", "age": 35})
# Replace an entire list
set_store("todos", [])
The store is read-only — writing directly via store.count = 5 raises an error. Always use set_store.
Produce¶
For batch mutations, produce provides an Immer-style API. The function receives a mutable draft:
from wybthon import produce
# Single mutation
set_store(produce(lambda s: setattr(s, "count", s.count + 1)))
# List mutations
set_store(produce(lambda s: s.todos.append({"id": 2, "text": "New", "done": False})))
# Multiple mutations in one pass
def update(s):
s.count = 42
s.user.name = "Updated"
set_store(produce(update))
The draft supports:
- Attribute read/write (
s.name,s.name = "new") - Item read/write (
s.items[0],s.items[0] = "x") appendandpopfor lists
Reactivity with effects¶
Store reads inside create_effect and render functions are tracked automatically:
from wybthon import create_effect
create_effect(lambda: print("Count is:", store.count))
# Prints: Count is: 0
set_store("count", 10)
# Prints: Count is: 10
Only effects that read the changed path re-run. Changing store.user.name won't re-trigger an effect that only reads store.count.
Using stores in components¶
Stores pair naturally with the @component decorator:
from wybthon import component, create_store, div, button, p, produce
@component
def TodoApp():
store, set_store = create_store({
"todos": [],
"next_id": 1,
})
def add_todo(e):
def update(s):
s.todos.append({"id": s.next_id, "text": f"Item {s.next_id}", "done": False})
s.next_id = s.next_id + 1
set_store(produce(update))
def toggle(idx):
return lambda e: set_store("todos", idx, "done", lambda d: not d)
def render():
return div(
button("Add", on_click=add_todo),
*[
p(
f"{'[x]' if todo.done else '[ ]'} {todo.text}",
on_click=toggle(i),
)
for i, todo in enumerate(store.todos)
],
)
return render
Stores vs signals¶
create_signal |
create_store |
|
|---|---|---|
| Best for | Primitive values, simple state | Nested objects, lists, complex state |
| Read | count() (call getter) |
store.count (attribute access) |
| Write | set_count(5) |
set_store("count", 5) |
| Nested | Manual (separate signals) | Automatic (path-based) |
| Granularity | Entire value | Per-property |