visit
With the praise has come a wave of curiosity. The most common question we’ve encountered is, “How does Rio actually work?” If you’ve been wondering the same thing, you’re in the right place! Here, we’ll explore the inner workings of Rio and uncover what makes it so powerful.
We will explore the following topics:
Among the numerous classes and functions in Rio, one stands out as indispensable: rio.Component
. This base class is omnipresent throughout the code, forming the foundation for every component in an app, including both user-defined and built-in components.
Components in Rio have several key responsibilities. Primarily, they manage the state of your app. For instance, when a user enters text into a rio.TextInput
, you'll need to store that value. Simply asking for user input without saving it won't make for a satisfying user experience. :)
To streamline value storage, all Rio components are designed to automatically function as dataclass
es.
class Cat:
def __init__(self, name: str, age: int, loves_catnip: bool) -> None:
self.name = name
self.age = age
self.loves_catnip = loves_catnip
This approach is functional but quite verbose. Notice how each attribute name must be repeated three times: once as the function parameter, once as the class attribute, and once to assign the parameter value to the attribute. The redundancy becomes even more cumbersome when inheritance is introduced:
class Animal:
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
class Cat(Animal):
def __init__(self, name: str, age: int, loves_catnip: bool) -> None:
super().__init__(name, age)
self.loves_catnip = loves_catnip
class Dog(Animal):
def __init__(self, name: str, age: int, is_good_boy: bool) -> None:
super().__init__(name, age)
self.is_good_boy = is_good_boy
from dataclasses import dataclass
@dataclass
class Animal:
name: str
age: int
@dataclass
class Cat(Animal):
loves_catnip: bool
@dataclass
class Dog(Animal):
is_good_boy: bool
Since Rio components need to store values and all inherit from rio.Component
, it made perfect sense to make them all dataclasses. This is why Rio components always come with the same, familiar structure:
class CatInput(rio.Component):
name: str
age: int
loves_catnip: bool
def build(self) -> rio.Component:
return ...
This structure should look familiar: attributes are defined at the top, there's no __init__
function, and a build
method is used to create the component's user interface. Whenever any of the attributes change, the build is called again, updating the component accordingly.
As we've just seen, one of the reasons Rio components are dataclasses is convenience. Notice how in the dataclass
version of Animal
, we are explicitly informing Python which fields the class has, and their types. For example, we're writing name: str
. Because of this, Python knows exactly that each animal will have a field named name
, and that the value of that field will always be a string.
Compare that to the regular class. We're telling Python that the __init__
function accepts a parameter called name
, and we copy that value into the class, but we never tell Python about it. The interpreter has no clue which fields the class comes with, and what their datatypes are.
Explicit fields are not only a good idea because they help out other developers reading your code, but because libraries such as Rio can read them as well. Because we have explicitly listed our fields, Rio now knows which values the class has, and will watch those for changes for us. How? Simple, using __setattr__
.
Python classes can have magic methods. You've seen these before, they're methods starting & ending with two underscores. __init__
is one of those methods.
Another method is __setattr__
. Python calls this function each time you assign a value to a class's attribute. You can use this to store values in a JSON value, whenever they're assigned to. Or maybe you're debugging code and want to print
new values when they change. Or, of course, maybe you want to be informed whenever an attribute changes, so you can rebuild the UI. This is exactly what Rio does.
class MyClass:
def __setattr__(self, name, value):
print(f"Setting {name} to {value}")
self.__dict__[name] = value
This is why it's crucial to always assign a new value to a component's attribute when you change it. If you modify an attribute without assignment (for example, by appending it to a list), Python won't trigger __setattr__
, and Rio won't know that your component needs to be updated. If your component seems unresponsive, check your code to ensure you are assigning new values to the attributes.
So, Rio has detected that your component has changed, build
has been called, and a new set of components has been created. What happens next? Should Rio simply discard the old components and replace them with new ones? No!
Instead, Rio uses a set of techniques called diffing & reconciliation. The first step is to find matches between the old and new components. For instance, if the build
function previously returned a TextInput
and returns a TextInput
again, it's likely the same component with an updated state. This process of finding matching pairs is known as diffing. The basic idea is simple: Rio recursively walks the new and old output of the build function.
You might have noticed a key
parameter in a Rio component before. This optional parameter is common to all components, and some even require you to set one.
If a component in the build
output has the same key as a component in the previous output, they are always considered a match, regardless of changes in their parent or overall position. This allows Rio to track components that have moved and update them accordingly.
Now, with matching pairs of components identified, the reconciliation step begins. Rio compares the paired components and determines which attributes to retain from each version:
These rules strike a good balance, always honoring new values while preserving the previous state if it was likely intentionally set.
This concludes the first installment of our deep dive series. The next part will be available in the coming weeks. In the meantime, join our Discord server! You can showcase the cool apps you've built or get help if you're still early in your journey.