github reflex-dev/reflex v0.11.0

45 minutes ago

Breaking Changes

  • @rx.event now returns an EventHandler instead of the decorated function. A decorator stacked above @rx.event is deprecated: the method still runs and registers under its own name, with a deprecation warning, until 1.0; apply other decorators below @rx.event. @rx.event on a method of a class that is not a state keeps working when called through an instance, and on a staticmethod or classmethod leaves it unchanged; both warn and should be removed, and calling such a method through the class (Helper.compute(obj, 3)) is not supported. A module-level function decorated with @rx.event whose first parameter is not a state stays a plain function and warns; drop the decorator from plain helper functions. Stacking @rx.event twice applies both, with the outer decorator's options winning; applying it to a handler already bound to another state, such as rx.event(OtherState.handler), raises TypeError, so decorate the function instead. (#7673, #7810)

  • deploy_url now defaults to None instead of http://localhost:3000, and in prod mode, when it is unset, the backend no longer sends CORS headers, so browsers cannot read cross-origin responses, and Socket.IO accepts same-origin connections only. With deploy_url set, that origin is allowed in addition to the backend's own. A split frontend and backend, a CDN-served frontend or a proxy that rewrites Host needs REFLEX_DEPLOY_URL set to the frontend's URL on the backend, for example REFLEX_DEPLOY_URL=https://app.example.com reflex run --env prod --backend-only. Previously any origin was allowed. Set cors_allowed_origins explicitly for other setups (["*"] restores the old behavior). Behind proxies, X-Forwarded-Host and X-Forwarded-Proto count only when trusted_proxy_hops is set, and the first rejected origin in each worker logs a warning naming the setting to change. Code that reads config.deploy_url must handle None: use the new config.frontend_url for the frontend's origin (deploy_url when set, http://localhost:<frontend_port> otherwise), or config.api_url when the backend serves the frontend from its own origin (single-port --env prod). (#7641, #7799)

  • A set or frozenset state var is now a SetVar rather than an ArrayVar, so it no longer type-checks as a sequence in frontend code: indexing, slicing, map, filter, join, reverse, +, *, the comparisons <, <=, > and >=, and contains with a field on it are deprecated. They still compile for now with a deprecation warning and will be removed in 1.0. Call .to_array() first:

    State.tags[0]  # deprecated
    State.tags.to_array()[0]
    ``` ([#7642](https://github.com/reflex-dev/reflex/issues/7642))
  • Browser storage vars (rx.Cookie, rx.LocalStorage, rx.SessionStorage) must be declared with a storage value as the default or with a storage type annotation. A default_factory on a storage var, or one returning a storage value, now fails at compile time instead of being silently ignored or treated as an ordinary var. (#7582)

  • Remove the built-in starlette-admin dashboard. rx.AdminDash(...) now raises an error linking to the admin dashboard guide, which shows how to mount starlette-admin yourself with api_transformer. (#7739)

  • Calling an rx.App instance (app()) returns a plain ASGI callable instead of a Starlette app, so the result no longer has add_middleware, routes or mount. Add Starlette middleware and routes with api_transformer instead. (#7624)

  • rx.ModelRegistry.register(Base) is now optional: until a base is registered, reflex db and the alembic command line migrate the tables of every DeclarativeBase subclass defined at module level or in a class body of a module the app imports. A base meant for another database, or one defined by an installed library, then gets create_table in the db_url migrations; register the bases of db_url with rx.ModelRegistry.register(Base) to leave the others out. An autogenerated revision that drops tables now logs a warning naming them. (#7797)

Deprecations

  • reflex.model.alembic_autogenerate, reflex.model.get_migration_history and reflex.model.format_revision, and the rx.Model.alembic_autogenerate and rx.Model.get_migration_history class methods, are deprecated and will be removed in 1.0.0. alembic_autogenerate now runs through the generated alembic/env.py and ignores its connection argument. Use alembic revision --autogenerate, alembic check and alembic history --indicate-current, or the matching reflex db commands, instead. (#7742)

Features

  • Event handlers are awaitable: await State.handler(args) inside another event handler runs it right away on the same locked state and returns its result, so a handler may return a value to the backend code awaiting it (returned events still chain, a returned coroutine still raises, and return values are not sent to the frontend yet). An awaited handler passes through app middleware like a queued event; if a middleware blocks it, the await returns None. Awaiting a handler outside a client's event, such as in an API route outside app.modify_state, raises EventHandlerTypeError. A non-event value returned by a handler that an event trigger such as on_click ran is dropped, and in dev mode a warning names the handler once; returning an uncalled handler or event creator, such as rx.redirect, still raises TypeError. self.handler() keeps calling the plain method as before.

    @rx.event
    def add(self, amount: int) -> int:
        self.total += amount
        return self.total
    
    
    @rx.event
    async def add_twice(self, amount: int):
        await type(self).add(amount)
        total = await type(self).add(amount)
        yield rx.toast(f"total is now {total}")
    ``` ([#7673](https://github.com/reflex-dev/reflex/issues/7673), [#7782](https://github.com/reflex-dev/reflex/issues/7782), [#7821](https://github.com/reflex-dev/reflex/issues/7821))
  • Add rx.client_state for client-only state. A named var (rx.client_state("", name="query")) is global: any component can read and write it, and the backend can push and retrieve it. An unnamed var is owned by the outermost page or @rx.memo that uses it, so each @rx.memo instance gets its own value and a page's values reset when it unmounts. Setting a var re-renders only the components that read it. .set takes a value, a FunctionVar, or a lambda (count.set(lambda v: v + 1)) and replaces .set_value, which is deprecated and will be removed in 1.0. A name must be an ASCII identifier without $, and declaring one name with two different defaults fails the compile, naming both declarations. rx._x.client_state remains available with a deprecation warning. (#6936, #7804)

  • Add rx.vars.SetVar for set[T] and frozenset[T] state vars, computed vars and literals, with contains, length, union, intersection, difference, symmetric_difference, issubset, issuperset, isdisjoint and to_array operations. Elements compare by value, so sets of tuples work. rx.foreach accepts a set var directly. frozenset is now a serializable state var type, and an optional set var (set[T] | None) holding None acts as an empty set in every operation. (#7642)

  • reflex db init now runs alembic init and installs an alembic/env.py that loads the app and reads db_url from rxconfig.py, and reflex db migrate, makemigrations and status are thin wrappers over alembic upgrade head, alembic revision --autogenerate and alembic history --indicate-current. The alembic command line works on the same environment, so alembic downgrade -1 needs no extra setup, and alembic revision --autogenerate and alembic check evaluate the pages as reflex db makemigrations does, so a model that only a page imports is compared too. In apps initialized by an earlier release, the next reflex db command or migrate() call replaces the stock env.py with the current template, keeping the previous file as env.old and the existing revisions; migrate() logs a warning when it does so. An env.py edited by hand is kept, with a warning that it is not the Reflex template; if it fails, reflex run logs the error and the reflex db commands exit with it. To regenerate it, move it aside and run reflex db init. (#7742, #7809)

  • In development, Reflex warns when an event handler or other task blocks the asyncio event loop for longer than event_loop_blocking_threshold seconds (default 0.2, REFLEX_EVENT_LOOP_BLOCKING_THRESHOLD), naming the handler and the blocking line of your code. Set it to 0 to turn detection off. (#7672)

  • React Compiler (the experimental react_compiler option) now also optimizes generated components that use id refs, generated UUIDs and memoized event handlers such as upload callbacks, installs the experimental babel-plugin-react-compiler release, and logs a warning that the option is experimental. Add the experimental auto_memoize option (REFLEX_AUTO_MEMOIZE): set auto_memoize=False together with react_compiler=True to let React Compiler memoize whole pages instead of Reflex's generated memo wrappers, which also shortens compilation; disabling both options logs a warning. Components with lifecycle events or hooks of their own keep a wrapper, so on_mount and on_unmount still follow their rx.cond branch. The refs object in $/utils/state is internal and not covered by compatibility guarantees; act on a component by id with event actions such as rx.set_value, rx.set_focus and rx.scroll_to. (#7694, #7806)

  • The db extra now installs aiosqlite, the async SQLite driver, so reflex[db] works with SQLAlchemy's async engine and sessions on the default SQLite database without a separate install. (#7679)

  • Allow redis-py 8. Reflex's own redis clients keep redis-py 7's behavior of no socket timeout. (#7757)

  • rx.foreach now warns at compile time when an item with lifecycle events (on_mount, on_unmount) or hooks of its own is keyed by its index, which is the default. After an item is removed, its on_unmount runs for the item at the last position; set a unique, stable key, such as key=item.id, or key=item for a list of unique strings, on the component the render function returns. (#7820)

Bug Fixes

  • When a foreground event handler raises or is cancelled (for example by supersedes), the state changes it made are now kept with the Redis state manager, as with the other state managers, and the changes made after its last yield are sent to the frontend right away. (#7412, #7696)
  • Fixed the redis state manager stalling a token's events until its lock expired after an event was cancelled (for example by supersedes) or failed, or after a lock release notification was lost. With REFLEX_OPLOCK_ENABLED, events queued on a lease no longer lose their updates when together they outlast the lock expiration, and events waiting on other instances are no longer passed over by the next lease. (#7534)
  • A background task whose async with on a state reached through get_state was cancelled, or failed while loading that state, no longer leaves the client's state locked, which hung every later event for that client and the server's shutdown. (#7738)
  • Apps passed to api_transformer now get ASGI lifespan events, so the lifespan (and on_startup/on_shutdown handlers) of a FastAPI or Starlette transformer run, and state the lifespan yields is available on request.state. Apps you mount on a transformer yourself still get none. If you worked around this by also registering the transformer's lifespan with register_lifespan_task, or by finding the transformer through the starlette_app lifespan task argument, remove the workaround: the first now runs the lifespan twice, and starlette_app is now the Starlette app serving Reflex's own routes. (#7624)
  • Using a backend var such as State._secret where a frontend value is needed now raises BackendVarFormatError, with the same explanation as f"{State._secret}", instead of a generic TypeError or ValueError, a later unrelated compile error, or the Field text in the page. This covers building a Var from it (rx.Var.create([State._secret]), rx.toast(State._secret), component props such as rx.image(src=State._secret)), the title, description and image of app.add_page(...), and converting it to text with str(), % or !s. In server code, print() or logging of a class-level backend var now raises too; use repr(State._secret) there instead. (#7651)
  • Match page routes case-insensitively on the backend, as React Router does on the frontend, so on_load events and router.page.path resolve for a URL like /About instead of falling back to the 404 page. (#7599)
  • Make production frontend builds reproducible: exporting the same app twice now gives identical files, so a deploy no longer renames scripts that did not change. (#7564)
  • Backend workers now load rxconfig.py before they import the app, so state_auto_setters=True generates set_<var> setters in workers started with spawn (Windows, macOS, REFLEX_STRICT_HOT_RELOAD, REFLEX_BACKEND_START_METHOD=spawn). (#7590)
  • Fix hooks of a component inside rx.foreach, such as on_mount, on_unmount, and hooks from add_hooks. A hook that used the loop variable crashed the page, and other hooks ran once for the whole list. Each item now runs its own hooks. A loop inside an @rx.memo function is not covered yet. (#7793)
  • After a hot reload in dev mode, cached vars restored by the disk or Redis state manager are recomputed with the new code instead of showing values from before the reload. State fields keep their values. (#7616)
  • An event larger than REFLEX_SOCKET_MAX_HTTP_BUFFER_SIZE sent over the polling transport no longer leaves the page unable to reconnect. (#7621)
  • Synced LocalStorage vars that share one storage name now update in the tab that writes, not only in other tabs. (#7637)
  • reflex run now stops the whole frontend process tree on SIGINT or SIGTERM, including a dev server left behind by npm run; a repeated signal no longer interrupts that cleanup, and a later Ctrl-C forces the exit when the cleanup is stuck. An exit the CLI asked for is no longer reported as a frontend failure. With --json, the app's process tree is killed if it has not exited 15 seconds after the first signal, and a consumer that reads slowly or stops reading can no longer hang shutdown or receive a cut JSON record. (#7650)
  • reflex run --frontend-only and reflex run --backend-only no longer exit with "Cannot specify --backend-port/--frontend-port" when rxconfig.py sets frontend_port or backend_port. Only a port flag given on the command line for the side that does not run is an error. (#7693)
  • reflex db init, reflex db makemigrations, and the schema check that runs after reflex db migrate and at app startup now evaluate every page before comparing the models, so a model that only a page imports is no longer missed from a generated revision or reported as a "Detected database schema changes" warning. (#7742)
  • reflex db migrate, reflex db makemigrations and reflex db status now report that db_url is not configured in rxconfig.py instead of failing inside alembic, and exit with status 1 when there is no database to migrate or it is not initialized. (#7742)
  • The Redis state manager no longer fails at backend shutdown with RuntimeError: ... attached to a different loop, or occasionally with Redis error deleting token, when the app is built on another event loop than the one serving it, as in AppHarness tests that use Playwright's sync API. (#7751)
  • With REFLEX_OPLOCK_ENABLED and several production backend workers, an event waiting for a token's lock on one worker no longer stalls for about a tenth of the lock expiration behind another worker's lease. (#7795)
  • In production, Granian now replaces a backend worker that dies (for example one killed by the OOM killer) instead of stopping the whole server; if a replaced worker dies again within a few seconds, the server still exits. Set GRANIAN_RESPAWN_FAILED_WORKERS=false to keep the old fail-fast behavior, for example when a process supervisor restarts the server. (#7803)
  • The deprecated rx._x.client_state now requires a name that is an ASCII identifier without $, as rx.client_state does, and an app whose pages declare one client state name with two different defaults fails to compile with an error naming both declarations. (#7804)
  • Write .web/package.json, and the copy kept in reflex.lock/package.json, in the layout bun uses, so the kept file no longer flips between formats and produces whitespace-only diffs. (#7812)
  • reflex db makemigrations refuses to add a NOT NULL column whose only default is a dataclass default_factory (mapped_column(default_factory=...) on a MappedAsDataclass model) and names the column, instead of writing a revision that fails on a table with rows. Give the column a server_default, a literal default=, or make it nullable. (#7815)

Performance

  • Lazy bundled libraries now load only the modules required by each dynamic component instead of loading every registered optional library at once. (#7276)
  • The backend no longer wraps the app in an extra Starlette application to run lifespan tasks, which removes a routing layer from every request. (#7624)

Don't miss a new reflex release

NewReleases is sending notifications on new releases.