StoreComponent and StoreComponentOp
Shared, mutable, app-wide state with explicit STORE / MERGE / CLEAN operations. Where a SignalComponent is a transient event bus, a StoreComponent is persistent state that callbacks read and write through op-dicts.
from weaverlet import StoreComponent, StoreComponentOp
StoreComponentOp: operation constants
class StoreComponentOp:
STORE = 'store'
MERGE = 'merge'
CLEAN = 'clean'
Use these constants when constructing op-dicts so typos surface at import time, not at runtime.
StoreComponent signature
class StoreComponent(WeaverletComponent):
store_attr: str = 'data'
store_id = Identifier()
_store_group_id = Identifier()
def __init__(self, name: str = "unnamed") -> None:
# Internal signals (don't reference externally):
self._clean_signal = SignalComponent(name='clean_signal')
self._store_signal = SignalComponent(name='store_signal')
self._merge_signal = SignalComponent(name='merge_signal')
# External input — emit op-dicts here:
self.input_signal = SignalComponent(name='input_signal')
def get_layout(self) -> "dash.html.Div": ...
def register_callbacks(self, app) -> None: ...
Public attributes
| Attribute | Type | Description |
|---|---|---|
store_id | Identifier | The underlying dcc.Store's element ID. Read via Input(store_id, store_attr) or SignalInput(store). |
store_attr | str ('data') | The property on the store. |
input_signal | SignalComponent | Write to the store by emitting an op-dict to this signal. |
The other _signal attributes are internal. Don't reference them externally.
Op-dicts
Emit one of these to SignalOutput(store.input_signal):
{"op": StoreComponentOp.STORE, "data": {...}} # overwrite the entire store value
{"op": StoreComponentOp.MERGE, "data": {...}} # shallow-merge dict into the store
{"op": StoreComponentOp.CLEAN} # reset the store to {}
The CLEAN op doesn't take a data key; the store is reset to {}.
Anything else (an unknown op value, or a missing op key) raises WeaverletException from the dispatch callback.
Usage
from weaverlet import (
WeaverletComponent, WeaverletApp, Identifier,
StoreComponent, StoreComponentOp,
SignalOutput,
)
from dash_extensions.enrich import Input, Output
from dash import html
class App(WeaverletComponent):
set_btn = Identifier()
merge_btn = Identifier()
clean_btn = Identifier()
display_id = Identifier()
def __init__(self):
super().__init__()
self.store = StoreComponent()
def get_layout(self):
return html.Div([
self.store(),
html.Button("Set theme=dark", id=self.set_btn),
html.Button("Merge user=Alice", id=self.merge_btn),
html.Button("Clean", id=self.clean_btn),
html.Pre(id=self.display_id),
])
def register_callbacks(self, app):
@app.callback(SignalOutput(self.store.input_signal),
Input(self.set_btn, "n_clicks"), prevent_initial_call=True)
def set_theme(_):
return {"op": StoreComponentOp.STORE, "data": {"theme": "dark"}}
@app.callback(SignalOutput(self.store.input_signal),
Input(self.merge_btn, "n_clicks"), prevent_initial_call=True)
def merge_user(_):
return {"op": StoreComponentOp.MERGE, "data": {"user": "Alice"}}
@app.callback(SignalOutput(self.store.input_signal),
Input(self.clean_btn, "n_clicks"), prevent_initial_call=True)
def clean(_):
return {"op": StoreComponentOp.CLEAN}
@app.callback(Output(self.display_id, "children"),
Input(self.store.store_id, "data"))
def show(state):
return f"Store: {state}"
WeaverletApp(root_component=App()).app.run()
How the multi-writer pattern works
Stock Dash rejects multiple callbacks targeting the same Output. StoreComponent works around this two ways:
-
External writers emit to
input_signal: aSignalComponentthat the store owns. Any number of external callbacks can target the sameSignalOutput(input_signal)becauseWeaverletApp.appis aDashProxywithgroup=multi-output multiplexing. -
Internal dispatch: a single callback in
register_callbackslistens toinput_signaland routes the op-dict to one of three internal signals (_store_signal,_merge_signal,_clean_signal). -
Three internal writers:
_store_signal,_merge_signal,_clean_signaleach have a callback that updatesOutput(store_id, "data"). All three share the samegroup=self._store_group_id, so theDashProxymultiplexer lets them coexist.
This indirection is invisible to you. Just emit op-dicts to input_signal and read store_id.
Pitfalls
_clean_signal, _store_signal, _merge_signal are implementation details. Wire your writers to input_signal only.
If you emit {"op": "foo"} or an op-dict without op, the dispatch callback raises WeaverletException. Always use the StoreComponentOp constants:
return {"op": StoreComponentOp.MERGE, "data": {...}} # correct
return {"op": "merge", "data": {...}} # works but typo-prone
MERGE is a shallow mergeNested dicts are replaced, not deep-merged:
# initial: {"settings": {"theme": "dark", "size": "lg"}}
# merge: {"data": {"settings": {"theme": "light"}}}
# result: {"settings": {"theme": "light"}} # size is gone!
If you need deep merge semantics, do the merge yourself before emitting, or use the STORE op with the full new state.
self.store() exactly onceThe StoreComponent carries multiple internal Identifier()s. Two renders → DuplicateIdError.