Global State
Managing Global State with the Observer Pattern
In this guide, we'll implement a global state management system using the Observer pattern. We'll create four counters, and when Counter 1 reaches 5, all counters will reset.
Subject and Observer in zenaura allows us to syncrhoinze multiple components to single subject. They will receive signals via update method.
This may sound complicated, but it actually simplify alot of use-cases for you, here is an example :
Step 1: Create needed presentational components
from zenaura.ui import h1, p, div, button
def Header1(text):
return h1(text)
def Paragraph(text, class_name=None):
return p(text, class_=class_name) if class_name else p(text)
def Div(class_name, children):
return div(*children, class_=class_name)
def Button(class_name, text, id=None, name=None):
return button(
text,
class_=class_name,
id=id,
name_=name
)
def CounterPresntaional(increaseBtn, headertext, count):
return div(
headertext,
increaseBtn,
id="large-header"
)
Step 2: Observer, Subject, Counter component
First, we need to import our Observer
abstract base class and our Subject
class that will manage the observers. Also we will define some presentational components that helps us build the counters.
importing the Observer
, Subject
.
importing presentational components
creating the Subject
class that will manage the observers.
and the CounterObserver
class that will react to state changes.
# Create the subject
counter_subject = Subject()
counter_subject.state = {"counter1": 0, "counter2": 0, "counter3": 0, "counter4": 0}
# create counter observer:
class CounterObserver(Observer):
pass
now we will create the counter component, where we manage a global state between different instances of it.
from zenaura.client.component import Resueable, Component
from zenaura.client.mutator import mutator, mutates
@Reuseable
class Counter(Component, CounterObserver):
def __init__(self, subject, counter_name):
super().__init__()
self.subject = subject
self.subject.attach(self)
self.counter_name = counter_name
@mutator
async def increment(self, event):
self.subject.state[self.counter_name] += 1
self.subject.notify()
@mutates
def update(self, value):
if self.subject.state["counter1"] == 5:
for k in self.subject.state.keys():
self.subject.state[k] = 0
def render(self):
return Div("container", [
CounterPresntaional(
Button("btn", "Increment", f"{self.counter_name}"),
Header1(f"count {self.subject.state[self.counter_name]}"),
self.subject.state[self.counter_name],
)
])
In init,
class Counter(Component, CounterObserver):
def __init__(self, subject, counter_name):
super().__init__()
self.subject = subject
self.subject.attach(self)
self.counter_name = counter_name
We link every instance of the counter to the global subject, since subject will only call counterInstance.update.
In increment,
@mutator
async def increment(self, event):
self.subject.state[self.counter_name] += 1
self.subject.notify()
we decorate with mutator so the component re-renders after increment called which is user event.
In update,
@mutates
def update(self, value):
if self.subject.state["counter1"] == 5:
for k in self.subject.state.keys():
self.subject.state[k] = 0
we implement the functionality of the global state, reseting all the counters when counter 1 reaches 5. Note we decorating update method with mutates, not mutator.
mutates is same as mutator , it will rerender the component, but it's for regualr python callbacks not coroutines.
Step 3: Create and Attach Counters to the Subject, create the page
Now, we'll create the counters and attach them to the subject. And create the page of the counters
Main Application
in main.py
:
from zenaura.client.app import App, Route
from zenaura.client.page import Page
from public.components import Counter, counter_subject
import asyncio
# Create counter components
counter1 = Counter(counter_subject, "counter1")
counter2 = Counter(counter_subject, "counter2")
counter3 = Counter(counter_subject, "counter3")
counter4 = Counter(counter_subject, "counter4")
## binds events :
dispatcher.bind("counter1", "click", counter1.increment)
dispatcher.bind("counter2", "click", counter2.increment)
dispatcher.bind("counter3", "click", counter3.increment)
dispatcher.bind("counter4", "click", counter4.increment)
# create the app
app = App()
app.add_route(
Route(
"Home",
"/",
Page(
[counter1, counter2, counter3, counter4]
)
)
)
# Run the application
app.run()
This will result in the required behavior, where the four counters are synced to a single global state as shown in the below GIF: