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.
Some examples can be found at in this clone link.
# 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
actionon the atom,
which updates the state of the atom,
which then updates any
selectorsthat depend on that state change; and finally,
rendermethods that depend on those updates are re-rendered.
Action → State change → Re-compute selectors → Call render methods
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 means that the render cycle will only be invoked
after all actions within the decorated function have been completed.
@action decorator can be used on any function and does not necessarily
need to be a method of an atom.
selector is a method of an atom that returns a value based on the atom’s state.
Essentially it is a getter method.
selector needs to do something expensive,
by say, combining various attributes of an atom, use the
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
render is any method/function that depends on the state of an atom,
or that depends on the return value of a
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
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
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
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 is any object that knows how to register subscribers and request renders.
To create an atom, use the
Whenever an attribute of an
atom is a
the attribute will be converted to a
Each is a subclass of
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.
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
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
@action decorator can be used on any function or method.
If the decorator is used above a method then the
atom used as the
can be caught within a
@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)
Show logging output for the module
Create an atom class. An atom class knows how to register subscribers and request re-renders when its state changes.
Create an atom class which is also a portable class. It is recommended to use the
@portable_atomdecorator over a combination of
renderdecorator anytime you want a function to depend on atom attributes or selectors. In a
rendermethod the attributes must be accessed explicitly. Whenever one of the attributes of the atom changes, the
rendermethod will be invoked.
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
@actiondecorator, 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
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, bound=None)
render(fn)(). Any atom attributes accessed within the body of the function will trigger a new call to the function when changed.
autorunreturns a dispose function. When the dispose function is called it stops any future renders of this
autoruncan 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)
reactionis similar to a
render. Changes in the
depends_on_fnwill force the
then_react_fnto be called. The
depends_on_fnis a function that takes no args. It should access any attributes that, when changed, should result in the call to the
depends_on_fnreturns a value that is not
None, this value will be passed to the
then_react_fn. If you need the previous result returned from
Truethen the call signature for
then_react_fnshould take 2 argments, the current return value and the previous return value from the
depends_on_fnwill fire immediately. But the
then_react_fnis only called the next time a dependency changes. To call the
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
See the example above for alternative approaches to updating
The reaction method returns a dispose function that can be called when you want to stop reactions.
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.
Stop a subscriber from running.
- class DictAtom
A subclass of
dict. Any attribute within an atom that is a
dictwill be converted to a
DictAtom. 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 a
listwill be converted to a
ListAtom. Renders that depend on the
ListAtomwill only be invoked if the
ListAtomchanges through methods like
- 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
todo_atom = Atom(done=False, description='walk the dog')
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
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.
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.
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.