Breaking Changes
-
@rx.eventnow returns anEventHandlerinstead of the decorated function. A decorator stacked above@rx.eventis 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.eventon a method of a class that is not a state keeps working when called through an instance, and on astaticmethodorclassmethodleaves 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.eventwhose first parameter is not a state stays a plain function and warns; drop the decorator from plain helper functions. Stacking@rx.eventtwice applies both, with the outer decorator's options winning; applying it to a handler already bound to another state, such asrx.event(OtherState.handler), raisesTypeError, so decorate the function instead. (#7673, #7810) -
deploy_urlnow defaults toNoneinstead ofhttp://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. Withdeploy_urlset, that origin is allowed in addition to the backend's own. A split frontend and backend, a CDN-served frontend or a proxy that rewritesHostneedsREFLEX_DEPLOY_URLset to the frontend's URL on the backend, for exampleREFLEX_DEPLOY_URL=https://app.example.com reflex run --env prod --backend-only. Previously any origin was allowed. Setcors_allowed_originsexplicitly for other setups (["*"]restores the old behavior). Behind proxies,X-Forwarded-HostandX-Forwarded-Protocount only whentrusted_proxy_hopsis set, and the first rejected origin in each worker logs a warning naming the setting to change. Code that readsconfig.deploy_urlmust handleNone: use the newconfig.frontend_urlfor the frontend's origin (deploy_urlwhen set,http://localhost:<frontend_port>otherwise), orconfig.api_urlwhen the backend serves the frontend from its own origin (single-port--env prod). (#7641, #7799) -
A
setorfrozensetstate var is now aSetVarrather than anArrayVar, so it no longer type-checks as a sequence in frontend code: indexing, slicing,map,filter,join,reverse,+,*, the comparisons<,<=,>and>=, andcontainswith afieldon 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. Adefault_factoryon 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 withapi_transformer. (#7739) -
Calling an
rx.Appinstance (app()) returns a plain ASGI callable instead of aStarletteapp, so the result no longer hasadd_middleware,routesormount. Add Starlette middleware and routes withapi_transformerinstead. (#7624) -
rx.ModelRegistry.register(Base)is now optional: until a base is registered,reflex dband thealembiccommand line migrate the tables of everyDeclarativeBasesubclass 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 getscreate_tablein thedb_urlmigrations; register the bases ofdb_urlwithrx.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_historyandreflex.model.format_revision, and therx.Model.alembic_autogenerateandrx.Model.get_migration_historyclass methods, are deprecated and will be removed in 1.0.0.alembic_autogeneratenow runs through the generatedalembic/env.pyand ignores itsconnectionargument. Usealembic revision --autogenerate,alembic checkandalembic history --indicate-current, or the matchingreflex dbcommands, 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 returnsNone. Awaiting a handler outside a client's event, such as in an API route outsideapp.modify_state, raisesEventHandlerTypeError. A non-event value returned by a handler that an event trigger such ason_clickran is dropped, and in dev mode a warning names the handler once; returning an uncalled handler or event creator, such asrx.redirect, still raisesTypeError.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_statefor client-only state. A named var (rx.client_state("", name="query")) is global: any component can read and write it, and the backend canpushandretrieveit. An unnamed var is owned by the outermost page or@rx.memothat uses it, so each@rx.memoinstance gets its own value and a page's values reset when it unmounts. Setting a var re-renders only the components that read it..settakes a value, aFunctionVar, or a lambda (count.set(lambda v: v + 1)) and replaces.set_value, which is deprecated and will be removed in 1.0. Anamemust be an ASCII identifier without$, and declaring one name with two different defaults fails the compile, naming both declarations.rx._x.client_stateremains available with a deprecation warning. (#6936, #7804) -
Add
rx.vars.SetVarforset[T]andfrozenset[T]state vars, computed vars and literals, withcontains,length,union,intersection,difference,symmetric_difference,issubset,issuperset,isdisjointandto_arrayoperations. Elements compare by value, so sets of tuples work.rx.foreachaccepts a set var directly.frozensetis now a serializable state var type, and an optional set var (set[T] | None) holdingNoneacts as an empty set in every operation. (#7642) -
reflex db initnow runsalembic initand installs analembic/env.pythat loads the app and readsdb_urlfrom rxconfig.py, andreflex db migrate,makemigrationsandstatusare thin wrappers overalembic upgrade head,alembic revision --autogenerateandalembic history --indicate-current. Thealembiccommand line works on the same environment, soalembic downgrade -1needs no extra setup, andalembic revision --autogenerateandalembic checkevaluate the pages asreflex db makemigrationsdoes, so a model that only a page imports is compared too. In apps initialized by an earlier release, the nextreflex dbcommand ormigrate()call replaces the stockenv.pywith the current template, keeping the previous file asenv.oldand the existing revisions;migrate()logs a warning when it does so. Anenv.pyedited by hand is kept, with a warning that it is not the Reflex template; if it fails,reflex runlogs the error and thereflex dbcommands exit with it. To regenerate it, move it aside and runreflex 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_thresholdseconds (default0.2,REFLEX_EVENT_LOOP_BLOCKING_THRESHOLD), naming the handler and the blocking line of your code. Set it to0to turn detection off. (#7672) -
React Compiler (the experimental
react_compileroption) now also optimizes generated components that useidrefs, generated UUIDs and memoized event handlers such as upload callbacks, installs the experimentalbabel-plugin-react-compilerrelease, and logs a warning that the option is experimental. Add the experimentalauto_memoizeoption (REFLEX_AUTO_MEMOIZE): setauto_memoize=Falsetogether withreact_compiler=Trueto 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, soon_mountandon_unmountstill follow theirrx.condbranch. Therefsobject in$/utils/stateis internal and not covered by compatibility guarantees; act on a component byidwith event actions such asrx.set_value,rx.set_focusandrx.scroll_to. (#7694, #7806) -
The
dbextra now installsaiosqlite, the async SQLite driver, soreflex[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.foreachnow 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, itson_unmountruns for the item at the last position; set a unique, stablekey, such askey=item.id, orkey=itemfor 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 lastyieldare 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. WithREFLEX_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 withon a state reached throughget_statewas 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_transformernow get ASGI lifespan events, so thelifespan(andon_startup/on_shutdownhandlers) of a FastAPI or Starlette transformer run, and state the lifespan yields is available onrequest.state. Apps you mount on a transformer yourself still get none. If you worked around this by also registering the transformer's lifespan withregister_lifespan_task, or by finding the transformer through thestarlette_applifespan task argument, remove the workaround: the first now runs the lifespan twice, andstarlette_appis now theStarletteapp serving Reflex's own routes. (#7624) - Using a backend var such as
State._secretwhere a frontend value is needed now raisesBackendVarFormatError, with the same explanation asf"{State._secret}", instead of a genericTypeErrororValueError, a later unrelated compile error, or theFieldtext in the page. This covers building a Var from it (rx.Var.create([State._secret]),rx.toast(State._secret), component props such asrx.image(src=State._secret)), thetitle,descriptionandimageofapp.add_page(...), and converting it to text withstr(),%or!s. In server code,print()or logging of a class-level backend var now raises too; userepr(State._secret)there instead. (#7651) - Match page routes case-insensitively on the backend, as React Router does on the frontend, so
on_loadevents androuter.page.pathresolve for a URL like/Aboutinstead 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.pybefore they import the app, sostate_auto_setters=Truegeneratesset_<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 ason_mount,on_unmount, and hooks fromadd_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.memofunction 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_SIZEsent over the polling transport no longer leaves the page unable to reconnect. (#7621) - Synced
LocalStoragevars that share one storagenamenow update in the tab that writes, not only in other tabs. (#7637) reflex runnow stops the whole frontend process tree on SIGINT or SIGTERM, including a dev server left behind bynpm 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-onlyandreflex run --backend-onlyno longer exit with "Cannot specify --backend-port/--frontend-port" whenrxconfig.pysetsfrontend_portorbackend_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 afterreflex db migrateand 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 makemigrationsandreflex db statusnow report thatdb_urlis 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 withRedis error deleting token, when the app is built on another event loop than the one serving it, as inAppHarnesstests that use Playwright's sync API. (#7751) - With
REFLEX_OPLOCK_ENABLEDand 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=falseto keep the old fail-fast behavior, for example when a process supervisor restarts the server. (#7803) - The deprecated
rx._x.client_statenow requires anamethat is an ASCII identifier without$, asrx.client_statedoes, 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 inreflex.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 makemigrationsrefuses to add a NOT NULL column whose only default is a dataclassdefault_factory(mapped_column(default_factory=...)on aMappedAsDataclassmodel) and names the column, instead of writing a revision that fails on a table with rows. Give the column aserver_default, a literaldefault=, 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
Starletteapplication to run lifespan tasks, which removes a routing layer from every request. (#7624)