Atomic
Object-oriented state management. The aim is to separate the state from the UI. Atoms manage the state. A form now has two jobs. It displays the state and hooks up events (like button clicks) to actions. The atom’s job is to manage the state. Each atom is a global object, which any form can import. By eliminating state from forms, there is no need to pass state up and down the form hierarchy. It also makes testing easier, since we can test atoms in isolation.
Examples
Some examples can be found at in this clone link.
Counter
# Create an atom that holds state
from anvil_labs.atomic import atom, action, selector
@atom
class CountAtom:
value = 0
@selector
def get_count(self):
return self.value
@action
def update_count(self, increment):
self.value += increment
count_atom = CountAtom()
# Create a form to display the count
from anvil_labs.atomic import render
from ..atoms.count import count_atom
class Count(CountTemplate):
def __init__(self):
self.display_count()
@render
def display_count(self):
# I get called any time the get_count return value changes
self.count_lbl.text = count_atom.get_count()
def neg_btn_click(self, **event_args):
count_atom.update_count(-1)
def pos_btn_click(self, **event_args):
count_atom.update_count(1)
In this example, whenever a button is clicked:
the button event handler calls an
action
on the atom,which updates the state of the atom,
which then updates any
selectors
that depend on that state change; and finally,any
render
methods that depend on those updates are re-rendered.
Action → State change → Re-compute selectors → Call render methods
Terminology
Action
An action is an expression/statement that updates the state of an atom.
Whenever the state changes, the atomic module invokes a render cycle.
We probably don’t want each state change to invoke a render cycle;
sometimes it makes sense to combine state updates into a single action.
To combine actions into a single action, use the @action
decorator.
Using the @action
decorator means that the render cycle will only be invoked
after all actions within the decorated function have been completed.
The @action
decorator can be used on any function and does not necessarily
need to be a method of an atom.
Selector
A selector
is a method of an atom that returns a value based on the atom’s state.
Essentially it is a getter method.
When a selector
needs to do something expensive,
by say, combining various attributes of an atom, use the @selector
decorator.
The return value from a decorated selector method is cached.
Whenever the atoms state changes, if the selector depends on that state,
the selector’s return value will be re-computed.
You should never update the state (call an action) within a selector
.
Render
A render
is any method/function that depends on the state of an atom,
or that depends on the return value of a selector
.
It’s most commonly used on methods within forms, but a render
can be used
outside of a form.
from anvil.js.window import document
from anvil_labs.atomic import render
@render
def update_tab_title():
document.title = count_atom.get_count()
update_tab_title()
Note we might want to do this with the autorun
function.
The above example is equivalent to.
from anvil_labs.atomic import autorun
def update_tab_title():
document.title = count_atom.get_count()
autorun(update_tab_title)
To depend on the state of an atom, the
render
method must explicitly access that state.
# BAD Example
class Count(CountTemplate):
def __init__(self):
self.display_count(count_atom.value)
@render
def display_count(self, count):
self.count_lbl.text = count
In the above example, the display_count
method does not explicitly access the count_atom.value
attribute.
This means it does not depend on this attribute. The code should look like this:
# GOOD Example
class Count(CountTemplate):
def __init__(self):
self.display_count()
@render
def display_count(self):
self.count_lbl.text = count_atom.value
Accessing an attribute/selector implicitly subscribes the render
method to changes in the state of those attributes/selectors.
Any time one of these attributes changes, the render
method is invoked (re-rendered).
You should never update the state (call an action) within a render
method.
If the render method is called by a component, it will only execute when the form is on the screen. This prevents renders from happening for cached forms, or forms that are no longer active.
Atom
An atom
is any object that knows how to register subscribers and request renders.
To create an atom, use the @atom
decorator.
Whenever an attribute of an atom
is a list
or dict
,
the attribute will be converted to a ListAtom
or DictAtom
.
Each is a subclass of list
/ dict
and behave as you’d expect.
The only difference is that these classes know how to register subscribers and request renders when their state changes.
Subscriber
A subscriber is an advanced feature. It’s the final part of the render cycle.
After all renders have been completed any subscribers that were decorated with the @subscribe
decorator will be called. A subscriber takes a single argument, a tuple of actions that were called to invoke the render cycle.
A reason to use a subscriber might be to update storage based on an action that was invoked.
Here’s an example.
from anvil_extras.storage import indexed_db
from anvil_labs.atomic import atom, action, subscribe
@atom
class Todos:
def __init__(self):
self.todos = indexed_db.get("todos", [])
@action(update_db=True)
def add_todo(self, todo):
self.todos = self.todos + [todo]
todos_atom = Todos()
@subscribe
def update_db_subscriber(actions):
if any(hasattr(action, "update_db") for action in actions):
indexed_db["todos"] = todos_atom.todos
The @action
decorator can be used on any function or method.
If the decorator is used above a method then the atom
used as the self
argument
can be caught within a subscribe
function
@subscribe
def update_db_subscriber(actions):
for action in actions:
if hasattr(action, "update_db"):
atom = action.atom
break
else:
return
# now use the atom to do something specific
...
Alternative Approaches to the subscriber
# ALTERNATIVE APPROACH 1 - use a render
is_first_run = True
@render
def update_db_with_render():
global is_first_run
todos = [dict(todo) for todo in todos_atom.todos]
# accessing the todos and each converting each todo to a dict
# creates a dependency on the todos and each key of each todo
# whenever these change this method is called
if is_first_run:
is_first_run = False
return
indexed_db["todos"] = todos
update_db_with_render()
# ALTERNATIVE APPROACH 2 - use autorun
def update_db_with_render():
# same code as above
...
autorun(update_db_with_render)
# ALTERNATIVE APPROACH 3 - use a reaction
def update_db_with_reaction(todos):
indexed_db["todos"] = todos
reaction(lambda: [dict(todo) for todo in todos_atom.todos], update_db_with_reaction)
# the first function sets up the dependencies
# the return value of this function is passed to the reaction function
# the reaction function is called only after the first change to any dependency
Bindings and Writeback
It’s not recommended to use anvil writebacks and data bindings with atoms. This is because we can’t control the render cycle.
Instead, there are two helper functions to create bindings and writebacks in code.
from anvil_labs.atomic import bind
class Count(CountTemplate):
def __init__(self):
bind(self.count_lbl, "text", count_atom.get_count)
# or bind it to an attribute of an atom
bind(self.count_lbl, "text", count_atom, "value")
The bind method is equivalent to:
def bind(component, prop, atom_or_selector, attr=None):
@render(bound=component)
def render_bind():
if callable(atom_or_selector):
setattr(component, prop, atom_or_selector())
elif isinstance(atom_or_selector, dict):
setattr(component, prop, atom_or_selector[attr])
else:
setattr(component, prop, getattr(atom_or_selector, attr))
render_bind()
Note the render decorator can take a bound parameter. This means that the render won’t fire if the component is not on the screen. This is not necessary when using the render decorator on a form method.
A writeback is similar to a bind, but a list of events must be provided.
writeback(self.check_box, "checked", self.item, "completed", events=["change"])
Alternatively, the writeback can be called with a selector in place of the atom and an action in place of the atom attribute. If the selector/action call signature is used, the action must take a single argument (the updated property of the component).
writeback(component, prop, atom, attr, events)
writeback(component, prop, selector, action, events)
API
- set_debug(debug=False)
Show logging output for the module
- @atom
Create an atom class. An atom class knows how to register subscribers and request re-renders when its state changes.
- @portable_atom
Create an atom class which is also a portable class. It is recommended to use the
@portable_atom
decorator over a combination of@atom
and@portable_class
.
- @render
- @render(bound=None)
Use the
render
decorator anytime you want a function to depend on atom attributes or selectors. In arender
method the attributes must be accessed explicitly. Whenever one of the attributes of the atom changes, therender
method will be invoked.
- @action
- @action(**kws)
The action decorator should be used above any method that you want to combine actions into a single action. A base action changes the state of an atom. When calling a function decorated with the
@action
decorator, the render cycle will be invoked only after all actions within the function have been executed. It’s worth noting that the decorator doesn’t need to be used unless you want to combine state updates into a single action. In the counter example, the action decorator is unnecessary, since there is only a single state update within the function (updating the.value
property)
- @selector
The selector decorator can only be used on methods within an atom. Its utility is caching the return value and a selector subscribes to atom attributes in a similar way to renders. If any attribute changes, the cached value will be re-computed. It’s worth noting that the selector decorator is unnecessary on methods where accessing the attribute is cheap. In the counter example, the selector is unnecessary and adds little to the implementation.
- autorun(fn)
- autorun(fn, bound=None)
Similar to
render(fn)()
. Any atom attributes accessed within the body of the function will trigger a new call to the function when changed.Calling
autorun
returns a dispose function. When the dispose function is called it stops any future renders of thisautorun
function.autorun
can be used as a decorator - but note that the returned function is not the original function but the dispose function.
- reaction(depends_on_fn, then_react_fn, *, fire_immediately=False, include_previous=False)
a
reaction
is similar to arender
. Changes in thedepends_on_fn
will force thethen_react_fn
to be called. Thedepends_on_fn
is a function that takes no args. It should access any attributes that, when changed, should result in the call to thethen_react_fn
.If
depends_on_fn
returns a value that is notNone
, this value will be passed to thethen_react_fn
. If you need the previous result returned fromdepends_on_fn
setinclude_previous=True
. Ifincldue_previous
isTrue
then the call signature forthen_react_fn
should take 2 argments, the current return value and the previous return value from thedepnds_on_fn
.depends_on_fn
will fire immediately. But thethen_react_fn
is only called the next time a dependency changes. To call thethen_react_fn
immediately setfire_immediately=True
.It would be rare to need to use this function.
However, in cases where you want to react to a change in an atom’s state that may result in a subsequent change, in another atom’s state a reaction may be useful. It can also be used as an alternative to
autorun
orrender
.See the example above for alternative approaches to updating
indexed_db
The reaction method returns a dispose function that can be called when you want to stop reactions.
- @subscribe
A subscriber is called after all re-renders resulting from a series of actions a subscriber takes a single argument - the tuple of actions that caused the re-render. See examples for use cases.
- unsubscribe(f)
Stop a subscriber from running.
- class DictAtom
A subclass of
dict
. Any attribute within an atom that is adict
will be converted to aDictAtom
. This allows render methods to depend on keys of dicts within the atom’s state.
- class ListAtom
A subclass of
list
. Any attribute within an atom that is alist
will be converted to aListAtom
. Renders that depend on theListAtom
will only be invoked if theListAtom
changes through methods likeremove()
,clear()
etc.
- class Atom(**kws)
A portable atom class that can be called with kwargs. Each kwarg will become an attribute of the atom. Useful if you prefer to access attributes rather than keys of a
DictAtom
.e.g.
todo_atom = Atom(done=False, description='walk the dog')
- ignore_updates
This can be used as a context manager (using
with
) to update an atom without invoking a render cycle. A reason to use this decorator is to lazy load an atom property. Use with caution.
Gotchas and advanced concepts
My component isn’t updating
Make sure that you have used the render decorator and that you have called this method from the __init__
function.
Why don’t you use self.init_components(**properties)
in the example?
The primary job of init_components
is to set up data bindings.
But since we don’t have any data bindings, we don’t need to use this method.
Note that init_components
does more work when used within a custom component.
How do I lazy load an attribute?
You can use the ignore_updates
decorator to prevent actions invoking render cycles.
And since calling an action within a render or selector is not allowed it becomes necessary.
import anvil.server
from anvil_labs.atomic import atom, ignore_updates, selector
@atom
class Todos:
def __init__(self):
self._todos = None
@property
@selector
def todos(self):
if self._todos is None:
with ignore_updates:
self._todos = anvil.server.call("get_todos")
return self._todos
Alternatively, you can call an action, ensuring that the action is not called inside a render/selector
from atoms import todos_atom
class Form1(Form1Template):
def __init__(self, **properties):
# fetch_todos is an action that calls the server if it needs to
todos_atom.fetch_todos()
self.display_todos()
My UI is taking a long time to update
That might be because you are calling a server function within an action. The fetch example is a good example of how to update the UI while you make a call.
@atom
class Fetch:
value = None
loading = False
@action
def set_status(self, value, loading=False):
self.value = value
self.loading = loading
def do_fetch(self):
self.set_status(None, loading=True)
ret = anvil.server.call_s("do_fetch")
self.set_status(ret, loading=False)
@selector
def get_info(self):
return self.value, self.loading
fetch_atom = Fetch()
do_fetch
is not an action, but set_status
is an action.
set_status
is cheap and so the UI updates quickly.
Each call to set_status
invokes a render cycle.
When loading
is True
the UI can disable a button while we call the server function.
How do I work with anvil data tables?
We’re working on it.