Upgrading to Reflex 0.10
This page covers the 0.10 changes most likely to break an existing app, or to be missed in the changelog, with the fix for each.
Reading a backend var on a state class
Reading a backend var on the state class, such as State._items, now returns its Field instead of the var's default value (#7312). Passing it to a component raises ChildrenTypeError (as a child) or TypeError: Unsupported type <class 'reflex_base.vars.base.Field'> for LiteralVar (as a prop), and interpolating it without a conversion in an f-string or with str.format() raises BackendVarFormatError (!r, !s and str() embed the Field text instead). Reading the var on a state instance, such as self._items, is unchanged.
Replace the class read with what the code meant:
from typing import ClassVar
import reflex as rx
class State(rx.State):
_items: list[str] = ["a", "b"]
_endpoint: ClassVar[str] = "https://example.com/api"
def page():
return rx.vstack(
# 0.9: rx.foreach(State._items, rx.text) baked in the default.
rx.foreach(State._items.default_value(), rx.text),
rx.text(State._endpoint),
)default_value() does not exist on 0.9.x, where State._items is already the plain list and State._items.default_value() raises AttributeError: 'list' object has no attribute 'default_value'. A package that supports both versions can read the field through get_fields(), which works on both:
State.get_fields()["_items"].default_value()Assigning a state var through its class
On 0.9, State.count = 10 replaced the class attribute, but new instances still started at the declared default. In 0.10 a state var is a descriptor on its class, and assigning over it raises TypeError instead of breaking the var. This includes pytest's monkeypatch.setattr and unittest.mock.patch.object on a var. Change the default with the field's set_default, patch the field in tests, and declare class-level configuration as ClassVar:
from typing import ClassVar
from unittest import mock
import httpx
import reflex as rx
class State(rx.State):
count: int = 0
_client: ClassVar[httpx.AsyncClient | None] = None
State.__fields__["count"].set_default(10)
State._client = httpx.AsyncClient() # a ClassVar stays an ordinary class attribute
with mock.patch.object(State.__fields__["count"], "default", 99):
...On 0.9, assigning State.__fields__["items"].default = [] was safe, because each instance got a copy of the default. In 0.10 every instance shares the field's default, so a mutable value assigned there leaks changes between sessions. set_default(default=[]) copies a mutable value for each instance.
Changing Defaults has the details, including browser storage vars.
Mutable defaults filled in after the class is defined
In 0.10 a mutable default given in the class body, or passed to set_default, is deep-copied when it is assigned (#7519). Changes made to the original object afterwards no longer reach new sessions. On 0.9, a module-level list filled in after the class statement still showed up in every new session; on 0.10 sessions start with the list as it was when the class was defined:
OPTIONS: list[str] = []
class State(rx.State):
options: list[str] = OPTIONS
OPTIONS.append("late") # 0.9: new sessions see ["late"]; 0.10: they see []Fill the value before the class is defined, or declare a factory that reads it when each session starts, which works on both versions:
class State(rx.State):
options: list[str] = rx.field(default_factory=lambda: list(OPTIONS))Calling inherited handlers from background tasks
In a background task, self is a proxy that only allows changes inside async with self. A handler declared on the same state was already called through that proxy, but a handler inherited from a parent state was not: on 0.9, self.inherited_handler() ran on the parent state without the lock, so it could write outside async with self. In 0.10 it goes through the proxy like any other handler (#7312):
- Outside
async with self, a call that modifies state raisesImmutableStateError; a read-only handler still runs. - Inside it,
selfin the called handler is the proxy, sotype(self)isStateProxy. Useself.__class__to get the state class;isinstance(self, Parent)still works.
Writing a var inherited from a parent state outside async with self now raises ImmutableStateError too, whether the task assigns it directly (self.count += 1) or through any handler. On 0.9 such a write raised nothing: with the in-memory state manager it landed without the lock, and with Redis it was silently lost. Changing a mutable inherited var in place, such as self.items.append(...), already raised on 0.9.
On 0.9 this background task wrote to the parent state without the lock; on 0.10 the first call raises ImmutableStateError:
class Parent(rx.State):
count: int = 0
@rx.event
def bump(self):
self.count += 1
class Child(Parent):
@rx.event(background=True)
async def work(self):
self.bump() # Raises ImmutableStateError on 0.10.Make the call inside async with self, which works on both versions:
class Child(Parent):
@rx.event(background=True)
async def work(self):
async with self:
self.bump()Sharing a state store between 0.9 and 0.10 instances
Instances of 0.9 and 0.10 cannot share a Redis or disk state store. A 0.10 instance loads state saved by 0.9, but a 0.9 instance discards state saved by 0.10 and starts the session over, so a rolling deploy that runs both versions, or a rollback to 0.9 against the same store, resets the sessions that reach the older instance. Upgrade every instance of an app together, and clear the store (or point the app at a fresh one) when rolling back. See Self Hosting for how the state store is shared between instances.
Other changes
EventHandler.is_backgroundandEventHandler.supersedesare read once per handler (#7370), so a decorator that marks a handler function withrx.event.BACKGROUND_TASK_MARKERorrx.event.SUPERSEDES_MARKERmust run before the handler is first used. A mark set afterwards is ignored without a warning.@rx.event(background=True)and@rx.event(supersedes=True)set it in time.- Assigning an undeclared attribute on a state, such as
self.typo = 1, still raisesSetUndefinedStateVarErrorin dev mode but no longer in prod (#7312). In prod it sets a plain attribute that is not a var, so changes to it are not tracked and never reach the client. reflex[db]allowssqlmodel0.0.45 and later (#7462), which stores a plaindatetimefield as UTC and rejects naive values. See Datetimes and SQLModel upgrades if your app stores naive datetimes.