Breaking Changes
pip install reflexno longer installspydantic; pydantic model support activates when it is installed. Use the newreflex[pydantic]extra (orreflex[db]) to keep it. (#6786)- The compiled frontend now targets React Router 8.3.0 (from 7.18.2), and Reflex requires Node 22.22.0 or newer as a result. Apps on the default generated setup need no
rxconfig.pyor app code changes. One change is required if you wrote a custom component againstreact-router-dom: that package no longer exists upstream and is no longer installed, solibrary = "react-router-dom"must becomereact-router(orreact-router/domforRouterProvider/HydratedRouter). (#6854) - A
RegistrationContextcan only be associated with a singleAppinstance, so creating a second barerx.App()in one process now raisesReflexRuntimeError(0.9.8 allowed it); use a freshRegistrationContext(e.g.RegistrationContext.fork()) to create multiple apps. (#6382)
reflex0.9.9.mp4
Deprecations
reflex.components.dynamic.bundled_librariesandDEFAULT_BUNDLED_LIBRARIESare deprecated (removal in 1.0) but keep working, resolving against the activeRegistrationContext. UseRegistrationContext.ensure_context().bundled_librariesto read the list, orbundle_library()/reset_bundled_libraries()to modify it. (#6967)reflex.page.DECORATED_PAGESis deprecated (removal in 1.0) but keeps working, resolving to a mapping of the app name to the activeRegistrationContext's page registrations. UseRegistrationContext.ensure_context().decorated_pagesinstead. (#6985)
Features
- The current
App, the loadedConfig,@rx.pageregistrations, and the bundled-library registry are now scoped to the activeRegistrationContextinstead of module-level globals, so multiple apps (and test harnesses) can coexist in one process without leaking registrations into each other. (#6382) - Report state deltas the frontend cannot process back to the backend via a new
client_errorsocket event, logging an actionable error in the terminal instead of failing silently. A frontend/backend state mismatch is fatal for the session: further events stop until the page is reloaded after the frontend is rebuilt orapi_urlis corrected. (#6827) - Framework logging now flows through standard python
loggingwith per-module loggers (reflex_base.utils.log, re-exported asreflex.utils.log), bootstrapped onimport reflex. Rich colored output is preserved, andREFLEX_LOG_JSONemits machine-readable JSON-lines records.--loglevel criticalno longer prints the system-info banner. (#6863) - The reflex CLI accepts
--json(equivalent toREFLEX_LOG_JSON) to emit machine-readable JSON-lines logs. (#6865) reflex deployaccepts--min-instancesand--max-instancesto set the autoscaling bounds of an app deployed to Google Cloud. Omitted bounds are left unchanged. (#6884)reflex deploygains--gcp-connection, to pick which of your organization's connected GCP accounts an app deploys through;--full-deploy, to serve the frontend from the provider's own container instead of Reflex's CDN; and--strategy, which was previously only settable in the config file. (#6908)- Compiled components are now named for React DevTools: memoized components take a
displayNamefrom the Python class or@rx.memofunction they came from instead of showing asAnonymous, generated contexts are named (StateContext(reflex___state____state.my_state).Providerrather than an unlabelledContext.Provider), pages are labelled with their route (Component(blog/[slug])), and client-only (NoSSRComponent) wrappers render asClientSide(<Tag>). (#6945)
Bug Fixes
- Stale
on_loadwork no longer blocks or outlives a page navigation: a newer navigation for the same client now cancels the previous page's unfinishedon_loadevent chain, includingon_loadhandlers that are background tasks (@rx.event(background=True)), which 0.9.8 let run to completion. Background tasks started from other events are unaffected. (#6593) - A
[[...splat]]catchall route no longer matches paths that merely share its prefix —posts/[[...splat]]matched/postsomethingas well as/postsand its descendants, so the wrong page'son_loadevents could fire. (#6790) - Ensure state manager instances use isolated internal locks instead of sharing one lock across instances. (#6830)
- Qualify
dictannotations onBaseStatethat were shadowed byBaseState.dict, so type checkers resolve them to the builtin. (#6846) reflex runnow pre-enables thedevelopmentexport condition for the dev server viaNODE_OPTIONS/BUN_OPTIONS, fixing the dev server exiting withrestartWithMergedOptions() was called, but the process has already been restartedon installs without node, where react-router 8's CLI re-executes itself to set the condition. (#6857)- An
AppHarnessProdno longer leaksREFLEX_ENV_MODE=prodto devAppHarnessinstances created later in the same process, which made them compile with route prerendering enabled and drop events dispatched during hydration recovery. (#6857) - Cache event handler annotations before runtime state-class patches can shadow builtin names on Python 3.14. (#6890)
rx.scripthead updates now flush synchronously instead of via react-helmet's requestAnimationFrame batching, fixing intermittently missing script tags after hydration (flaky "scripts not loaded" failures). (#6905)- Fixed a race where a finishing background task could silently discard state updates made by a concurrently running event handler before they reached the frontend, leaving the UI stale until the next write. Background handlers that never enter
async with selfstill emit their delta, now computed under the state lock. (#6920) AppHarnessstarts the frontend dev server with thedevelopmentexport condition enabled, fixing "Frontend did not start" on node-less (bun-only) installs where react-router's dev CLI restart guard trips. (#6931)- Adding a page no longer raises a spurious
RouteValueErrorwhen a static segment lines up with another route's dynamic segment (e.g./posts/all/[x]alongside/posts/[id]). React Router resolves such siblings in favor of the static one, so only two differently named dynamic segments at the same position conflict. The check was also order-dependent: it only tripped when the bracket-carrying route was added second. (#6953) - Reduce published wheel and sdist size by removing misplaced generated artifacts. (#6966)
- A
client_errorsocket emit with no payload no longer raises an unhandledTypeErrorinside python-socketio's dispatch, which let any connected socket — even one without a valid token — spam asyncio tracebacks into the backend logs past the handler's rate limits. (#6984) - Console warnings and errors no longer print literal backslash-escaped brackets (e.g.
dict\[str, str]). The rich-markup escapes were left over from the legacy console helpers, but the logging pipeline renders messages with markup disabled, so bracketed type names now print verbatim.VarAttributeErrormessages drop the same escapes. (#6989) reflex runno longer hangs forever when a fatal error (e.g. the node minimum-version check on the npm path) exits the frontend worker thread while the backend blocks the main thread; the failure now interrupts the main thread and the CLI exits promptly with the original error. (#6990, #6994)
Performance
- Remove the per-update
asyncio.create_taskwrapper inEventNamespace.emit_update, cutting scheduling overhead roughly in half for every outgoing state update. (#6734) - Dev mode no longer pays for React's per-element owner-stack capture: navigation clicks in a large app dropped from ~350ms to ~83ms of main-thread CPU (5.6x prod down to ~1.3x). In exchange
React.captureOwnerStack()returns no owner frames in dev, which affects React DevTools' owner-stack view and custom error overlays built on that API; setREFLEX_REACT_OWNER_STACKS=1to restore them. (#6905) @rx.memocomponents with props bound to state are now auto-memoized at the call site: the state hooks those props need compile into a generated wrapper component instead of the page module. A state change re-renders that wrapper rather than the whole page, and React'smemostops there unless one of the prop values actually changed. (#6949)- The generated
vite.config.jsnow declares a hook filter on the plugin that redirectsreact-dom/servertoreact-dom/server.node, so the bundler no longer calls into it for every import in the module graph — on the Reflex docs site that was ~15,800 calls per build to rewrite a single specifier. (#6959)
Documentation
- Documented the
provider,gcp_connectionandfull_deploycloud config settings, including which settings a Google Cloud target ignores and whyfull_deployis left unset rather than false by default. (#6908)
Miscellaneous
- The generated
package.jsonno longer carries a framework-ownedpostcssoverride; the pinnedpostcssdev dependency already forces a single resolved copy for every transitive requirer. Projects that already installed 0.9.8 keep an inert"postcss": "8.5.23"override inreflex.lock/package.json; it matches the dev-dependency pin, so it changes nothing today and can be deleted by hand. (#6854) - Upgrade the locked dev tooling:
ruff0.15.12 -> 0.16.2,pyright1.1.408 -> 1.1.411,typer0.25.1 -> 0.27.1. (#6893) - The
reflex deploycommand implementation moved out of thereflexpackage intoreflex-hosting-cli, so cloud code is no longer shipped inside the framework. Flags and behavior are unchanged, andreflex-hosting-cliremains a dependency ofreflex, soreflex deployandreflex cloudstay available out of the box. If the package is not installed, these commands now report which package to install instead of failing with a missing-command error. (#6924)