Reflex lets you describe the screen in Python. It compiles that description into a React app. Your browser runs the screen; your Python server runs the stateful event handlers.
Think of a restaurant: the browser shows the menu and takes your order. The Python server is the kitchen. An event is the order ticket. A state update is the kitchen telling the screen what changed. Below, we open the ticket, follow the kitchen code, and inspect the exact reply.
The entire story fits in two pages
Home /
Hello, Ada!
Name input: Ada
Count: 0 · Doubled: 0
Button: Add one
Typing invokes rename(value). Clicking invokes increment().
Loading invokes enter_home().
Details /details
Same state, another route
Shows the same greeting and count.
Has another increment button.
Navigating invokes enter_details(). The state survives the route change;
visits increase.
The server starts with name="Ada", count=0, visits=0, and page="Not loaded". The first home on-load changes
page to "Home" and visits to 1. greeting depends on
name; doubled depends on count. _handled is a backend-only counter,
deliberately absent from public state snapshots.
Run this checkout's example
# From the original Reflex development checkout:
cd examples/flow_lab
uv run reflex run --frontend-port 3037 --backend-port 8037
# App: http://localhost:3037
# This guide: http://localhost:3037/walkthrough.html Run uv sync at the repository root first if dependencies are missing. This app
uses the repository's uv environment. The guide also opens directly as an HTML file and needs
no CDN or backend. The interactive explorer below is a teaching simulation; the separate Python
app is the live framework example.
Full runnable Python source — not pseudocode
"""Follow a name and a counter from Python to the browser and back."""
import reflex as rx
class LabState(rx.State):
"""Per-browser data shared by both pages."""
name: str = "Ada"
count: int = 0
visits: int = 0
page: str = "Not loaded"
_handled: int = 0
@rx.var
def greeting(self) -> str:
"""Return the greeting derived from the current name.
Returns:
A greeting for the current visitor.
"""
return f"Hello, {self.name}!"
@rx.var
def doubled(self) -> int:
"""Return twice the current counter.
Returns:
The counter multiplied by two.
"""
return self.count * 2
@rx.event
def rename(self, value: str):
"""Store the text received from the input.
Args:
value: The input's current text.
"""
self.name = value
self._handled += 1
@rx.event
def increment(self):
"""Increase the counter by one."""
self.count += 1
self._handled += 1
@rx.event
def enter_home(self):
"""Record a visit to the home page."""
self.page = "Home"
self.visits += 1
@rx.event
def enter_details(self):
"""Record a visit to the details page."""
self.page = "Details"
self.visits += 1
def layout(content: rx.Component) -> rx.Component:
"""Wrap either page in shared navigation and state readouts.
Args:
content: The page-specific content.
Returns:
The complete page component.
"""
return rx.center(
rx.vstack(
rx.heading("Reflex flow lab", size="8"),
rx.text("One Python state. Two routes. Watch every round trip."),
rx.hstack(rx.link("Home", href="/"), rx.link("Details", href="/details")),
rx.text("Server page: ", LabState.page, " · Page loads: ", LabState.visits),
rx.text("State synchronized: ", rx.cond(rx.State.is_hydrated, "yes", "no")),
rx.separator(),
content,
rx.separator(),
rx.link(
"Open the illustrated walkthrough ↗",
href="/walkthrough.html",
is_external=True,
),
spacing="5",
width="100%",
max_width="680px",
padding="32px",
),
min_height="100vh",
)
def index() -> rx.Component:
"""Build the home page at compile time.
Returns:
The greeting, editable name, and counter controls.
"""
return layout(
rx.vstack(
rx.heading(LabState.greeting, id="greeting"),
rx.text("Your name"),
rx.input(
value=LabState.name,
on_change=LabState.rename,
id="name",
aria_label="Your name",
),
rx.text("Count: ", LabState.count, id="count"),
rx.text("Doubled: ", LabState.doubled, id="doubled"),
rx.button("Add one", on_click=LabState.increment, id="increment"),
width="100%",
spacing="4",
)
)
def details() -> rx.Component:
"""Build a second route that reads the same state.
Returns:
The details page with a counter control.
"""
return layout(
rx.vstack(
rx.heading("Same state, another route"),
rx.text(LabState.greeting),
rx.text("Count: ", LabState.count, " · Doubled: ", LabState.doubled),
rx.button("Add one here too", on_click=LabState.increment),
spacing="4",
)
)
app = rx.App()
app.add_page(index, route="/", on_load=LabState.enter_home)
app.add_page(details, route="/details", on_load=LabState.enter_details)
Configuration
"""Configuration for the data-flow teaching app."""
import reflex as rx
config = rx.Config(
app_name="flow_lab",
plugins=[rx.plugins.RadixThemesPlugin()],
disable_plugins=[rx.plugins.SitemapPlugin],
)
Python makes the instructions. React runs the screen.
← State deltas / frontend commands
The page function does not run on the Python server every time you click. For this app it builds a component description during compilation. A click runs an event handler against a server state instance. React then renders using the received state. This differs from rendering a whole new HTML document for each event.
There are two services in the usual development setup: a Vite/React Router frontend serving the page and modules, and an ASGI backend handling Socket.IO and API endpoints. Production serves built frontend assets and a backend, often behind a reverse proxy. Single-port mode can put both behind one port. This repository uses Starlette for the backend path; older descriptions may refer to a different stack.
How your Python becomes browser code
From Python objects to a runnable browser app
Follow the artifacts. UI declarations produce React modules; Python event handlers stay on the backend.
RequestResponseLocal workExecute the page-building code
Python calls component constructors. This creates objects describing the interface, not HTML in a browser.
Timeline & playback controls
Local processing in Python process
Execute the page-building code
rx.button("Add one", on_click=LabState.increment, id="increment") Actual source/output excerpts where identified; illustrative timing. These are compilation artifacts, not event payloads. Full modules and build output are available below.
Illustrative timing, not measured latency. Pause or drag the timeline to inspect any phase. The original source and real captured frames remain below.
Two outputs from one Python app
Frontend output
React components, event callbacks, state providers, CSS, routing code, and static assets. The browser downloads JavaScript and runs it with React.
button callback → event name + arguments
context lookup → public state value
component render → React elements → DOMBackend runtime
The original Python modules, imported classes/functions, registered handlers, and per-client state. An ASGI server runs these objects in a Python process.
Event name → registered Python method
client token → state instance
self.count += 1 → public state delta1. Python executes the UI description
When the compiler evaluates index(), Python executes the calls to rx.vstack, rx.text, and rx.button. Each call
creates a Python component object containing properties, children, style information,
imports, and event declarations. The result is an in-memory tree. It is not a screenshot,
DOM tree, HTML string, or browser state instance.
# A component description constructed by Python:
rx.button("Add one", on_click=LabState.increment, id="increment")
# Conceptual tree (shortened):
Center
└── VStack
├── Heading("Reflex flow lab")
├── Text("Count: ", Var(LabState.count))
└── Button("Add one", on_click=EventHandler(LabState.increment)) Python can run ordinary loops or call helper functions to build this static structure.
State-dependent browser branches use rx.cond and rx.foreach:
those represent future frontend expressions. An ordinary Python if LabState.count > 0 cannot choose a future visitor’s UI at compile time.
2. A class-level state reference is a symbolic expression
In a page definition, LabState.count is a Var: an object
representing a value that will be read from a frontend state context. Reflex tracks the
expression’s Python type and the imports/hooks/state dependencies needed to render it. It
is different from self.count inside a running handler, which is the current Python
integer for that session.
# Page-building time:
LabState.count
# Browser expression for this example:
reflex___state____state__flow_lab___flow_lab____lab_state.count_rx_state_
# Handler execution time:
self.count # e.g. the Python integer 1 The compiler registers a context dependency so the generated React component can use useContext. That is why a counter can update without rerunning the Python page function. The
class-level expression is a description of a read; it is not a request to send the current
value during compilation.
3. Render metadata becomes JavaScript source
The compiler collects imports, hooks, custom code, dynamic imports, and render metadata.
In this checkout, _compile_page passes them to page_template.
Components can be extracted into memoized modules. The renderer emits JavaScript
expressions such as jsx(RadixThemesButton, props, children).
For the example button, the wrapper supplies the React component import. The event
declaration supplies a callback that sends a handler identifier. It does not copy the body of increment() into JavaScript. The actual intermediate dictionary
and generated code are shown in the artifact viewer below.
// Readable equivalent; names simplified:
const onClick = event => addEvents([
ReflexEvent("reflex___state____state.flow_lab___flow_lab____lab_state.increment")
]);
return jsx(RadixThemesButton, { id: "increment", onClick }, "Add one");
// This Python statement has NOT been translated into the callback:
// self.count += 1 jsx(...) is a JavaScript function call used by React’s rendering ecosystem. A .jsx filename does not mean every generated file must contain literal <Button> tags. Our generated files already use function calls. The toolchain
still needs to resolve imports and prepare the module graph for browser delivery.
4. Contexts, initial data, routes, and styles are generated too
compile_state creates a framework-initialized state instance and serializes
its initial public values. The context template writes these defaults, a context/reducer
per substate, and the dispatch mapping into .web/utils/context.js. These are
build-time defaults—not a particular visitor’s saved state.
The app root installs the providers and routing structure. Route modules import their
components. Styles become CSS or style expressions consumed by the React components; the
bundler also includes styles from component libraries. References such as $/utils/state are project aliases resolved by the frontend toolchain, while imports such as react resolve to installed packages.
5. The frontend toolchain makes runnable browser assets
Reflex’s compiler produces the frontend project. React Router and Vite then process that project: they resolve dependencies, transform modules as needed, split code into chunks, and emit production JavaScript/CSS plus a document. In development, Vite serves transformed modules on demand. In production, the host serves the built files.
I built the example’s existing generated frontend to make this concrete. The build
transformed 417 client modules and produced build/client/index.html, hashed
JavaScript chunks and styles. A manifest is also emitted during bundling; the React Router
SPA build cleans intermediate build files. The full example chunk and HTML below are
actual build output. Their filenames and contents are a snapshot; a later build can change
them.
The build log also mentions an ssr environment and temporarily creates a
React Router server bundle. With this app’s ssr: false configuration, the build
uses that machinery to generate the SPA document. That bundle is not a translation of your
Python handlers, and its existence is not evidence that each visitor receives personalized
Python-rendered HTML.
6. The browser loads modules and React creates the UI
The HTML points to JavaScript assets. The browser parses the document, fetches modules and their dependencies, and its JavaScript engine executes them. React’s entry point attaches the application to the document. Context providers initialize from the generated defaults; React evaluates the component functions and commits DOM nodes. CSS controls their appearance. Socket hydration then synchronizes the public state with the Python session.
HTTP document → asset URLs → JavaScript modules
→ React entry point → providers + route component
→ React elements → DOM nodes → layout and paint
→ Socket.IO hydration → current session values The Python Button wrapper is not shipped as a Python object. It became an import
plus React component code. The rendered UI may include library classes, accessibility attributes,
and wrapper elements, so the literal final HTML is not necessarily a one-to-one transcription
of your Python component calls.
What is stored where?
| Location | Concrete form | Lifetime / purpose |
|---|---|---|
| Application source | flow_lab/flow_lab.py, rxconfig.py | Authored Python on disk. Imported by compilation and backend startup. |
| Compiler process memory | Python component objects, Var expressions, registries, render dictionaries, generated source strings | Build-time representations. Not automatically persisted as an executable Python-to-JS cache or sent as application payloads. |
.web/app_components/ and .web/app/routes/ | Generated React/JavaScript source modules | Input to the frontend toolchain. Generated files should be changed through Python source, not hand-edited as the source of truth. |
.web/utils/context.js | JavaScript defaults, contexts, reducers, and event setup | Initial frontend state shape and update wiring. Contains public defaults, not every visitor’s data. |
.web/node_modules/ | Installed React, component libraries, build tools, and their dependencies | Build/development dependencies. The browser receives the needed transformed/bundled code, not the whole directory. |
.web/build/client/ | Production HTML, hashed JS/CSS, static assets | Browser-facing files generated by this build. Browser/proxy caches may retain them according to serving headers. |
| Python backend memory | Imported modules, function/code objects, handler registry, app/ASGI objects, state-manager objects | Long-lived runtime handling incoming events. CPython executes compiled Python
bytecode/code objects; an optional __pycache__ is an import cache, not
a frontend asset. |
| State-manager storage | Memory objects, serialized disk state, or Redis data, depending on configuration | Per-client state retention. This is separate from generated JS. Server-side state serialization is distinct from the public JSON wire representation. |
| Browser memory / storage | JavaScript modules, React contexts, DOM; session token in sessionStorage | UI execution and public data. Reload discards the current JS/DOM instances, then initialization and hydration rebuild them. |
What form is the code in on the Python server?
# This stays Python. The backend imports it normally.
@rx.event
def increment(self):
self.count += 1
self._handled += 1
# Conceptual runtime dispatch, NOT literal framework source:
handler = registered_handlers[event.name]
state = state_manager.get_state(client_token)
handler(state, **event.payload)
# Framework resolves deltas and emits them. The decorator and registration machinery make the method addressable by an event name. The method’s code and the state instance’s values are separate objects: the code describes what to do, and the session instance holds the current count/name. A stored session file is not a compiled frontend module or a copy of the handler’s source.
For an ordinary counter click, there is no Vite build, Python page-function evaluation, or rewritten JavaScript file. Python mutates state and sends a delta; the already-loaded frontend renders it. Editing the Python UI source is different: the development workflow can re-evaluate pages, regenerate frontend code, and refresh/reload the browser as configured.
Inspect the actual compilation artifacts
The original generated frontend and socket capture belong to the guide’s September 9
snapshot. The button intermediate representation was inspected against checkout 35c11e12c, and the existing generated frontend was successfully production-built on September 13.
These are explicitly identified snapshots, not a claim that the saved frontend was
regenerated from today’s checkout.
Intermediate render dictionary — actual button.render() output
{
"name": "RadixThemesButton",
"props": [
"id:\"increment\"",
"onClick:((_e) => (addEvents([(ReflexEvent(\"reflex___state____state.flow_lab___flow_lab____lab_state.increment\", ({ }), ({ })))], [_e], ({ }))))",
"ref:ref_increment"
],
"children": [
{
"contents": "\"Add one\""
}
]
}Generated button React module — actual excerpt
export const Button_button_5b96ea87d6d46c65a36c4e74327104a9_4c100443 = memo(({children}) => {
const ref_increment = useRef(null); refs["ref_increment"] = ref_increment;
const on_click_f878fea372bb8c958470a4b8a1cc88df = useCallback(((_e) => (addEvents([(ReflexEvent("reflex___state____state.flow_lab___flow_lab____lab_state.increment", ({ }), ({ })))], [_e], ({ })))), [addEvents, ReflexEvent])
return(
jsx(RadixThemesButton,{id:"increment",onClick:on_click_f878fea372bb8c958470a4b8a1cc88df,ref:ref_increment},children)
)
});
Button_button_5b96ea87d6d46c65a36c4e74327104a9_4c100443.displayName = "Button";Generated contexts and defaults — actual excerpts
export const initialState = {"reflex___state____state": {"is_hydrated_rx_state_": false, "router_headers_rx_state_": {"host": "", "origin": "", "upgrade": "", "connection": "", "cookie": "", "pragma": "", "cache_control": "", "user_agent": "", "sec_websocket_version": "", "sec_websocket_key": "", "sec_websocket_extensions": "", "accept_encoding": "", "accept_language": "", "raw_headers": {}}, "router_page_rx_state_": {"host": "", "path": "", "raw_path": "", "full_path": "", "full_raw_path": "", "params": {}}, "router_route_id_rx_state_": "", "router_session_rx_state_": {"client_token": "", "client_ip": "", "session_id": ""}, "router_url_rx_state_": {"scheme": "", "netloc": "", "origin": "", "path": "", "query": "", "query_parameters": {}, "fragment": "", "href": ""}}, "reflex___state____state.flow_lab___flow_lab____lab_state": {"count_rx_state_": 0, "doubled_rx_state_": 0, "greeting_rx_state_": "Hello, Ada!", "name_rx_state_": "Ada", "page_rx_state_": "Not loaded", "visits_rx_state_": 0}, "reflex___state____state.reflex___istate___shared____shared_state_base_internal": {}, "reflex___state____state.reflex___state____frontend_event_exception_state": {}, "reflex___state____state.reflex___state____on_load_internal_state": {}, "reflex___state____state.reflex___state____update_vars_internal_state": {}}
export const UploadFilesContext = createContext(null);
export const DispatchContext = createContext(null);
export const StateContexts = {reflex___state____state: createContext(null),reflex___state____state__flow_lab___flow_lab____lab_state: createContext(null),reflex___state____state__reflex___istate___shared____shared_state_base_internal: createContext(null),reflex___state____state__reflex___state____frontend_event_exception_state: createContext(null),reflex___state____state__reflex___state____on_load_internal_state: createContext(null),reflex___state____state__reflex___state____update_vars_internal_state: createContext(null),};
export const EventLoopContext = createContext(null);
const [reflex___state____state, dispatch_reflex___state____state] = useReducer(applyDelta, initialState["reflex___state____state"])
const [reflex___state____state__flow_lab___flow_lab____lab_state, dispatch_reflex___state____state__flow_lab___flow_lab____lab_state] = useReducer(applyDelta, initialState["reflex___state____state.flow_lab___flow_lab____lab_state"])
const [reflex___state____state__reflex___istate___shared____shared_state_base_internal, dispatch_reflex___state____state__reflex___istate___shared____shared_state_base_internal] = useReducer(applyDelta, initialState["reflex___state____state.reflex___istate___shared____shared_state_base_internal"])
const [reflex___state____state__reflex___state____frontend_event_exception_state, dispatch_reflex___state____state__reflex___state____frontend_event_exception_state] = useReducer(applyDelta, initialState["reflex___state____state.reflex___state____frontend_event_exception_state"])
const [reflex___state____state__reflex___state____on_load_internal_state, dispatch_reflex___state____state__reflex___state____on_load_internal_state] = useReducer(applyDelta, initialState["reflex___state____state.reflex___state____on_load_internal_state"])
const [reflex___state____state__reflex___state____update_vars_internal_state, dispatch_reflex___state____state__reflex___state____update_vars_internal_state] = useReducer(applyDelta, initialState["reflex___state____state.reflex___state____update_vars_internal_state"])Generated Home route — actual complete module
import {Flex as RadixThemesFlex,Heading as RadixThemesHeading,Link as RadixThemesLink,Separator as RadixThemesSeparator,Text as RadixThemesText} from "@radix-ui/themes"
import {Link as ReactRouterLink} from "react-router"
import {Bare_comp_08b86a9f1bd7a593973d476aa345bb48_4c100443,Bare_comp_3b3be27a1da5f94255775fbf381c65c6_4c100443,Bare_comp_54b99599c937d55ee52fd2f5e8ce87e1_4c100443,Bare_comp_a04c3d7fb1724c249acaa687c753f068_4c100443,Bare_comp_bd80ed2b11892f25e4ebf1ecc0c92c00_4c100443,Bare_comp_fdcf0bd4ec792dfdb7c993a1ed3aa361_4c100443,Button_button_5b96ea87d6d46c65a36c4e74327104a9_4c100443,Debounceinput_debounceinput_c4ee43471c4a3b824243638911cd8d18_4c100443} from "$/app_components/flow_lab/flow_lab"
import {Fragment,useEffect,useRef} from "react"
import {refs} from "$/utils/state"
import {jsx} from "@emotion/react"
function Component() {
const ref_greeting = useRef(null); refs["ref_greeting"] = ref_greeting;
const ref_count = useRef(null); refs["ref_count"] = ref_count;
const ref_doubled = useRef(null); refs["ref_doubled"] = ref_doubled;
return (
jsx(Fragment,{},jsx(RadixThemesFlex,{css:({ ["display"] : "flex", ["alignItems"] : "center", ["justifyContent"] : "center", ["minHeight"] : "100vh" })},jsx(RadixThemesFlex,{align:"start",className:"rx-Stack",css:({ ["width"] : "100%", ["maxWidth"] : "680px", ["padding"] : "32px" }),direction:"column",gap:"5"},jsx(RadixThemesHeading,{size:"8"},"Reflex flow lab"),jsx(RadixThemesText,{as:"p"},"One Python state. Two routes. Watch every round trip."),jsx(RadixThemesFlex,{align:"start",className:"rx-Stack",direction:"row",gap:"3"},jsx(RadixThemesLink,{asChild:true,css:({ ["&:hover"] : ({ ["color"] : "var(--accent-8)" }) })},jsx(ReactRouterLink,{to:"/"},"Home")),jsx(RadixThemesLink,{asChild:true,css:({ ["&:hover"] : ({ ["color"] : "var(--accent-8)" }) })},jsx(ReactRouterLink,{to:"/details"},"Details"))),jsx(RadixThemesText,{as:"p"},"Server page: ",jsx(Bare_comp_08b86a9f1bd7a593973d476aa345bb48_4c100443,{},)," \u00b7 Page loads: ",jsx(Bare_comp_54b99599c937d55ee52fd2f5e8ce87e1_4c100443,{},)),jsx(RadixThemesText,{as:"p"},"State synchronized: ",jsx(Bare_comp_a04c3d7fb1724c249acaa687c753f068_4c100443,{},)),jsx(RadixThemesSeparator,{size:"4"},),jsx(RadixThemesFlex,{align:"start",className:"rx-Stack",css:({ ["width"] : "100%" }),direction:"column",gap:"4"},jsx(RadixThemesHeading,{id:"greeting",ref:ref_greeting},jsx(Bare_comp_bd80ed2b11892f25e4ebf1ecc0c92c00_4c100443,{},)),jsx(RadixThemesText,{as:"p"},"Your name"),jsx(Debounceinput_debounceinput_c4ee43471c4a3b824243638911cd8d18_4c100443,{},),jsx(RadixThemesText,{as:"p",id:"count",ref:ref_count},"Count: ",jsx(Bare_comp_fdcf0bd4ec792dfdb7c993a1ed3aa361_4c100443,{},)),jsx(RadixThemesText,{as:"p",id:"doubled",ref:ref_doubled},"Doubled: ",jsx(Bare_comp_3b3be27a1da5f94255775fbf381c65c6_4c100443,{},)),jsx(Button_button_5b96ea87d6d46c65a36c4e74327104a9_4c100443,{},"Add one")),jsx(RadixThemesSeparator,{size:"4"},),jsx(RadixThemesLink,{asChild:true,css:({ ["&:hover"] : ({ ["color"] : "var(--accent-8)" }) })},jsx(ReactRouterLink,{target:(true ? "_blank" : ""),to:"/walkthrough.html"},"Open the illustrated walkthrough \u2197")))),jsx("title",{},"FlowLab | Index"),jsx("meta",{content:"favicon.ico",property:"og:image"},))
)
}
Component.displayName = "Component(index)";
export default Component;
Production browser JavaScript — actual flow_lab-lNesF4hv.js
import{G as e,W as t,q as n}from"./components-Blt2PyNA.js";import{$ as r,a as i,it as a,n as o,ot as s,st as c,tt as l}from"./text-field-BkrB7aRw.js";import{n as u}from"./emotion-react.browser.esm-Do-RBW0K.js";var d=e(((e,t)=>{var n=NaN,r=/^\s+|\s+$/g,i=/^[-+]0x[0-9a-f]+$/i,a=/^0b[01]+$/i,o=/^0o[0-7]+$/i,s=parseInt,c=typeof global==`object`&&global&&global.Object===Object&&global,l=typeof self==`object`&&self&&self.Object===Object&&self,u=c||l||Function(`return this`)(),d=Object.prototype.toString,f=Math.max,p=Math.min,m=function(){return u.Date.now()};function h(e,t,n){var r,i,a,o,s,c,l=0,u=!1,d=!1,h=!0;if(typeof e!=`function`)throw TypeError(`Expected a function`);t=y(t)||0,g(n)&&(u=!!n.leading,d=`maxWait`in n,a=d?f(y(n.maxWait)||0,t):a,h=`trailing`in n?!!n.trailing:h);function _(t){var n=r,a=i;return r=i=void 0,l=t,o=e.apply(a,n),o}function v(e){return l=e,s=setTimeout(S,t),u?_(e):o}function b(e){var n=e-c,r=e-l,i=t-n;return d?p(i,a-r):i}function x(e){var n=e-c,r=e-l;return c===void 0||n>=t||n<0||d&&r>=a}function S(){var e=m();if(x(e))return C(e);s=setTimeout(S,b(e))}function C(e){return s=void 0,h&&r?_(e):(r=i=void 0,o)}function w(){s!==void 0&&clearTimeout(s),l=0,r=c=i=s=void 0}function T(){return s===void 0?o:C(m())}function E(){var e=m(),n=x(e);if(r=arguments,i=this,c=e,n){if(s===void 0)return v(c);if(d)return s=setTimeout(S,t),_(c)}return s===void 0&&(s=setTimeout(S,t)),o}return E.cancel=w,E.flush=T,E}function g(e){var t=typeof e;return!!e&&(t==`object`||t==`function`)}function _(e){return!!e&&typeof e==`object`}function v(e){return typeof e==`symbol`||_(e)&&d.call(e)==`[object Symbol]`}function y(e){if(typeof e==`number`)return e;if(v(e))return n;if(g(e)){var t=typeof e.valueOf==`function`?e.valueOf():e;e=g(t)?t+``:t}if(typeof e!=`string`)return e===0?e:+e;e=e.replace(r,``);var c=a.test(e);return c||o.test(e)?s(e.slice(2),c?2:8):i.test(e)?n:+e}t.exports=h})),f=e((e=>{function n(e){"@babel/helpers - typeof";return n=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},n(e)}Object.defineProperty(e,"__esModule",{value:!0}),e.DebounceInput=void 0;var r=o(t()),i=o(d()),a=[`element`,`onChange`,`value`,`minLength`,`debounceTimeout`,`forceNotifyByEnter`,`forceNotifyOnBlur`,`onKeyDown`,`onBlur`,`inputRef`];function o(e){return e&&e.__esModule?e:{default:e}}function s(e,t){if(e==null)return{};var n=c(e,t),r,i;if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(i=0;i<a.length;i++)r=a[i],!(t.indexOf(r)>=0)&&Object.prototype.propertyIsEnumerable.call(e,r)&&(n[r]=e[r])}return n}function c(e,t){if(e==null)return{};for(var n={},r=Object.keys(e),i,a=0;a<r.length;a++)i=r[a],!(t.indexOf(i)>=0)&&(n[i]=e[i]);return n}function l(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),n.push.apply(n,r)}return n}function u(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]==null?{}:arguments[t];t%2?l(Object(n),!0).forEach(function(t){S(e,t,n[t])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):l(Object(n)).forEach(function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))})}return e}function f(e,t){if(!(e instanceof t))throw TypeError(`Cannot call a class as a function`)}function p(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,`value`in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function m(e,t,n){return t&&p(e.prototype,t),n&&p(e,n),Object.defineProperty(e,"prototype",{writable:!1}),e}function h(e,t){if(typeof t!=`function`&&t!==null)throw TypeError(`Super expression must either be null or a function`);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),Object.defineProperty(e,"prototype",{writable:!1}),t&&g(e,t)}function g(e,t){return g=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},g(e,t)}function _(e){var t=b();return function(){var n=x(e),r;if(t){var i=x(this).constructor;r=Reflect.construct(n,arguments,i)}else r=n.apply(this,arguments);return v(this,r)}}function v(e,t){if(t&&(n(t)===`object`||typeof t==`function`))return t;if(t!==void 0)throw TypeError(`Derived constructors may only return object or undefined`);return y(e)}function y(e){if(e===void 0)throw ReferenceError(`this hasn't been initialised - super() hasn't been called`);return e}function b(){if(typeof Reflect>`u`||!Reflect.construct||Reflect.construct.sham)return!1;if(typeof Proxy==`function`)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){})),!0}catch{return!1}}function x(e){return x=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)},x(e)}function S(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}var C=function(e){h(n,e);var t=_(n);function n(e){var r;f(this,n),r=t.call(this,e),S(y(r),`onChange`,function(e){e.persist();var t=r.state.value,n=r.props.minLength;r.setState({value:e.target.value},function(){var i=r.state.value;if(i.length>=n){r.notify(e);return}t.length>i.length&&r.notify(u(u({},e),{},{target:u(u({},e.target),{},{value:``})}))})}),S(y(r),`onKeyDown`,function(e){e.key===`Enter`&&r.forceNotify(e);var t=r.props.onKeyDown;t&&(e.persist(),t(e))}),S(y(r),`onBlur`,function(e){r.forceNotify(e);var t=r.props.onBlur;t&&(e.persist(),t(e))}),S(y(r),`createNotifier`,function(e){if(e<0)r.notify=function(){return null};else if(e===0)r.notify=r.doNotify;else{var t=(0,i.default)(function(e){r.isDebouncing=!1,r.doNotify(e)},e);r.notify=function(e){r.isDebouncing=!0,t(e)},r.flush=function(){return t.flush()},r.cancel=function(){r.isDebouncing=!1,t.cancel()}}}),S(y(r),`doNotify`,function(){r.props.onChange.apply(void 0,arguments)}),S(y(r),`forceNotify`,function(e){var t=r.props.debounceTimeout;if(!(!r.isDebouncing&&t>0)){r.cancel&&r.cancel();var n=r.state.value,i=r.props.minLength;n.length>=i?r.doNotify(e):r.doNotify(u(u({},e),{},{target:u(u({},e.target),{},{value:n})}))}}),r.isDebouncing=!1,r.state={value:e.value===void 0||e.value===null?``:e.value};var a=r.props.debounceTimeout;return r.createNotifier(a),r}return m(n,[{key:`componentDidUpdate`,value:function(e){if(!this.isDebouncing){var t=this.props,n=t.value,r=t.debounceTimeout,i=e.debounceTimeout,a=e.value,o=this.state.value;n!==void 0&&a!==n&&o!==n&&this.setState({value:n}),r!==i&&this.createNotifier(r)}}},{key:`componentWillUnmount`,value:function(){this.flush&&this.flush()}},{key:`render`,value:function(){var e=this.props,t=e.element;e.onChange,e.value,e.minLength,e.debounceTimeout;var n=e.forceNotifyByEnter,i=e.forceNotifyOnBlur,o=e.onKeyDown,c=e.onBlur,l=e.inputRef,d=s(e,a),f=this.state.value,p=n?{onKeyDown:this.onKeyDown}:o?{onKeyDown:o}:{},m=i?{onBlur:this.onBlur}:c?{onBlur:c}:{},h=l?{ref:l}:{};return r.default.createElement(t,u(u(u(u({},d),{},{onChange:this.onChange,value:f},p),m),h))}}]),n}(r.default.PureComponent);e.DebounceInput=C,S(C,`defaultProps`,{element:`input`,type:`text`,onKeyDown:void 0,onBlur:void 0,value:void 0,minLength:0,debounceTimeout:100,forceNotifyByEnter:!0,forceNotifyOnBlur:!0,inputRef:void 0})})),p=e(((e,t)=>{var n=f().DebounceInput;n.DebounceInput=n,t.exports=n})),m=n(t(),1),h=n(p(),1),g=(0,m.memo)(({children:e})=>(0,m.useContext)(r.reflex___state____state__flow_lab___flow_lab____lab_state).page_rx_state_);g.displayName=`Bare`;var _=(0,m.memo)(({children:e})=>(0,m.useContext)(r.reflex___state____state__flow_lab___flow_lab____lab_state).visits_rx_state_);_.displayName=`Bare`;var v=(0,m.memo)(({children:e})=>(0,m.useContext)(r.reflex___state____state).is_hydrated_rx_state_?`yes`:`no`);v.displayName=`Bare`;var y=(0,m.memo)(({children:e})=>(0,m.useContext)(r.reflex___state____state__flow_lab___flow_lab____lab_state).greeting_rx_state_);y.displayName=`Bare`;var b=(0,m.memo)(({children:e})=>{let t=(0,m.useRef)(null);c.ref_name=t;let n=(0,m.useCallback)((e=>l([a(`reflex___state____state.flow_lab___flow_lab____lab_state.rename`,{value:e?.target?.value},{})],[e],{})),[l,a]),i=(0,m.useContext)(r.reflex___state____state__flow_lab___flow_lab____lab_state);return u(h.default,{"aria-label":`Your name`,debounceTimeout:300,element:o,id:`name`,inputRef:t,onChange:n,value:s(i.name_rx_state_)?i.name_rx_state_:``})});b.displayName=`DebounceInput`;var x=(0,m.memo)(({children:e})=>(0,m.useContext)(r.reflex___state____state__flow_lab___flow_lab____lab_state).count_rx_state_);x.displayName=`Bare`;var S=(0,m.memo)(({children:e})=>(0,m.useContext)(r.reflex___state____state__flow_lab___flow_lab____lab_state).doubled_rx_state_);S.displayName=`Bare`;var C=(0,m.memo)(({children:e})=>{let t=(0,m.useRef)(null);c.ref_increment=t;let n=(0,m.useCallback)((e=>l([a(`reflex___state____state.flow_lab___flow_lab____lab_state.increment`,{},{})],[e],{})),[l,a]);return u(i,{id:`increment`,onClick:n,ref:t},e)});C.displayName=`Button`;var w=(0,m.memo)(({children:e})=>{let t=(0,m.useCallback)((e=>l([a(`reflex___state____state.flow_lab___flow_lab____lab_state.increment`,{},{})],[e],{})),[l,a]);return u(i,{onClick:t},e)});w.displayName=`Button`;export{y as a,w as c,v as i,b as l,S as n,x as o,_ as r,C as s,g as t};Production SPA HTML — actual build/client/index.html
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta content="width=device-width, initial-scale=1" name="viewport"/><link rel="modulepreload" href="/assets/manifest-95bcf033.js"/><link rel="modulepreload" href="/assets/entry.client-5UErbQKK.js"/><link rel="modulepreload" href="/assets/components-Blt2PyNA.js"/><link rel="modulepreload" href="/assets/errorBoundaries-DIYo_grw.js"/><link rel="modulepreload" href="/assets/root-gPZAYZ4h.js"/><link rel="modulepreload" href="/assets/text-field-BkrB7aRw.js"/><link rel="modulepreload" href="/assets/reflex-env-Bgj8Vl-s.js"/><link rel="modulepreload" href="/assets/emotion-react.browser.esm-Do-RBW0K.js"/><script>
// Only run in browser environment, not during SSR
if (typeof document !== 'undefined') {
try {
const theme = localStorage.getItem("theme") || 'system';
const systemPreference = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
const resolvedTheme = theme === "system" ? systemPreference : theme;
// Apply theme immediately - blocks until complete
// Use classList to avoid overwriting other classes
document.documentElement.classList.remove("light", "dark");
document.documentElement.classList.add(resolvedTheme);
document.documentElement.style.colorScheme = resolvedTheme;
} catch (e) {
// Fallback to system preference on any error (resolve "system" to actual theme)
const fallbackTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
document.documentElement.classList.remove("light", "dark");
document.documentElement.classList.add(fallbackTheme);
document.documentElement.style.colorScheme = fallbackTheme;
}
}
</script><link href="/assets/__reflex_global_styles-NccINl-w.css" rel="stylesheet" type="text/css"/></head><body><section aria-label="Notifications alt+T" tabindex="-1" aria-live="polite" aria-relevant="additions text" aria-atomic="false" data-react-aria-top-layer="true"></section><div data-is-root-theme="true" data-accent-color="blue" data-gray-color="slate" data-has-background="true" data-panel-background="translucent" data-radius="medium" data-scaling="100%" class="radix-themes css-0"><style data-emotion="css 17rg0dp">.css-17rg0dp{position:fixed;width:100vw;height:0;}</style><div title="Connection Error: " class="css-17rg0dp"></div></div><script>window.__reactRouterContext = {"basename":"/","future":{"unstable_enableNodeReadableStream":false,"unstable_optimizeDeps":true},"routeDiscovery":{"mode":"initial"},"ssr":false,"isSpaMode":true};window.__reactRouterContext.stream = new ReadableStream({start(controller){window.__reactRouterContext.streamController = controller;}}).pipeThrough(new TextEncoderStream());</script><script type="module" async="">import "/assets/manifest-95bcf033.js";
import * as route0 from "/assets/root-gPZAYZ4h.js";
window.__reactRouterRouteModules = {"root":route0};
import("/assets/entry.client-5UErbQKK.js");</script><!--$--><script>window.__reactRouterContext.streamController.enqueue("[{\"_1\":2,\"_3\":-5,\"_4\":-5},\"loaderData\",{},\"actionData\",\"errors\"]\n");</script><!--$--><script>window.__reactRouterContext.streamController.close();</script><!--/$--><!--/$--></body></html>- Import configuration and app. Python evaluates the state class and
registers event handlers, variable types, defaults, and computed-variable dependencies.
app.add_pagerecords routes and on-load handlers. - Evaluate the page functions.
index()returns a tree of components. A reference such asLabState.countis a symbolic Var expression at this point, not a particular visitor's integer. - Compile component properties and children. Reflex collects imports, styles, hooks, event argument expressions, and dependencies. It generates React code, memoized component modules, route modules, and state context providers.
- Generate initial state. The compiler serializes default state into
.web/utils/context.js. This is a build-time starting point, not a lookup of the visitor's saved state. - Serve or bundle. Development serves/transforms modules with Vite. Production builds JavaScript/CSS/assets. The browser downloads those files and executes JavaScript; it does not run the Python page functions.
Python declaration
rx.text("Count: ", LabState.count)
rx.button("Add one", on_click=LabState.increment)
rx.input(value=LabState.name,
on_change=LabState.rename)Readable equivalent of generated JS
// Simplified names and structure, not verbatim output.
const state = useContext(StateContexts.lab);
jsx(Text, { children: ["Count: ", state.count_rx_state_] });
// The DOM event is used locally to extract the argument.
addEvents([ReflexEvent(LAB + ".rename", {
value: event.target.value
})]);Actual generated component module from this app
import {ReflexEvent,applyEventActions,isNotNullOrUndefined,isTrue,refs} from "$/utils/state"
import {StateContexts,addEvents} from "$/utils/context"
import {Fragment,memo,useCallback,useContext,useEffect,useRef} from "react"
import {jsx} from "@emotion/react"
import DebounceInput from "react-debounce-input"
import {Button as RadixThemesButton,TextField as RadixThemesTextField} from "@radix-ui/themes"
export const Bare_comp_08b86a9f1bd7a593973d476aa345bb48_4c100443 = memo(({children}) => {
const reflex___state____state__flow_lab___flow_lab____lab_state = useContext(StateContexts.reflex___state____state__flow_lab___flow_lab____lab_state)
return(
reflex___state____state__flow_lab___flow_lab____lab_state.page_rx_state_
)
});
Bare_comp_08b86a9f1bd7a593973d476aa345bb48_4c100443.displayName = "Bare";
export const Bare_comp_54b99599c937d55ee52fd2f5e8ce87e1_4c100443 = memo(({children}) => {
const reflex___state____state__flow_lab___flow_lab____lab_state = useContext(StateContexts.reflex___state____state__flow_lab___flow_lab____lab_state)
return(
reflex___state____state__flow_lab___flow_lab____lab_state.visits_rx_state_
)
});
Bare_comp_54b99599c937d55ee52fd2f5e8ce87e1_4c100443.displayName = "Bare";
export const Bare_comp_a04c3d7fb1724c249acaa687c753f068_4c100443 = memo(({children}) => {
const reflex___state____state = useContext(StateContexts.reflex___state____state)
return(
(reflex___state____state.is_hydrated_rx_state_ ? "yes" : "no")
)
});
Bare_comp_a04c3d7fb1724c249acaa687c753f068_4c100443.displayName = "Bare";
export const Bare_comp_bd80ed2b11892f25e4ebf1ecc0c92c00_4c100443 = memo(({children}) => {
const reflex___state____state__flow_lab___flow_lab____lab_state = useContext(StateContexts.reflex___state____state__flow_lab___flow_lab____lab_state)
return(
reflex___state____state__flow_lab___flow_lab____lab_state.greeting_rx_state_
)
});
Bare_comp_bd80ed2b11892f25e4ebf1ecc0c92c00_4c100443.displayName = "Bare";
export const Debounceinput_debounceinput_c4ee43471c4a3b824243638911cd8d18_4c100443 = memo(({children}) => {
const ref_name = useRef(null); refs["ref_name"] = ref_name;
const on_change_7b9c23851e47f1545854e812eeac6a5d = useCallback(((_e) => (addEvents([(ReflexEvent("reflex___state____state.flow_lab___flow_lab____lab_state.rename", ({ ["value"] : _e?.["target"]?.["value"] }), ({ })))], [_e], ({ })))), [addEvents, ReflexEvent])
const reflex___state____state__flow_lab___flow_lab____lab_state = useContext(StateContexts.reflex___state____state__flow_lab___flow_lab____lab_state)
return(
jsx(DebounceInput,{"aria-label":"Your name",debounceTimeout:300,element:RadixThemesTextField.Root,id:"name",inputRef:ref_name,onChange:on_change_7b9c23851e47f1545854e812eeac6a5d,value:(isNotNullOrUndefined(reflex___state____state__flow_lab___flow_lab____lab_state.name_rx_state_) ? reflex___state____state__flow_lab___flow_lab____lab_state.name_rx_state_ : "")},)
)
});
Debounceinput_debounceinput_c4ee43471c4a3b824243638911cd8d18_4c100443.displayName = "DebounceInput";
export const Bare_comp_fdcf0bd4ec792dfdb7c993a1ed3aa361_4c100443 = memo(({children}) => {
const reflex___state____state__flow_lab___flow_lab____lab_state = useContext(StateContexts.reflex___state____state__flow_lab___flow_lab____lab_state)
return(
reflex___state____state__flow_lab___flow_lab____lab_state.count_rx_state_
)
});
Bare_comp_fdcf0bd4ec792dfdb7c993a1ed3aa361_4c100443.displayName = "Bare";
export const Bare_comp_3b3be27a1da5f94255775fbf381c65c6_4c100443 = memo(({children}) => {
const reflex___state____state__flow_lab___flow_lab____lab_state = useContext(StateContexts.reflex___state____state__flow_lab___flow_lab____lab_state)
return(
reflex___state____state__flow_lab___flow_lab____lab_state.doubled_rx_state_
)
});
Bare_comp_3b3be27a1da5f94255775fbf381c65c6_4c100443.displayName = "Bare";
export const Button_button_5b96ea87d6d46c65a36c4e74327104a9_4c100443 = memo(({children}) => {
const ref_increment = useRef(null); refs["ref_increment"] = ref_increment;
const on_click_f878fea372bb8c958470a4b8a1cc88df = useCallback(((_e) => (addEvents([(ReflexEvent("reflex___state____state.flow_lab___flow_lab____lab_state.increment", ({ }), ({ })))], [_e], ({ })))), [addEvents, ReflexEvent])
return(
jsx(RadixThemesButton,{id:"increment",onClick:on_click_f878fea372bb8c958470a4b8a1cc88df,ref:ref_increment},children)
)
});
Button_button_5b96ea87d6d46c65a36c4e74327104a9_4c100443.displayName = "Button";
export const Button_button_95e42ef5e6e269cf3d8d5b17cfe4e82a_4c100443 = memo(({children}) => {
const on_click_f878fea372bb8c958470a4b8a1cc88df = useCallback(((_e) => (addEvents([(ReflexEvent("reflex___state____state.flow_lab___flow_lab____lab_state.increment", ({ }), ({ })))], [_e], ({ })))), [addEvents, ReflexEvent])
return(
jsx(RadixThemesButton,{onClick:on_click_f878fea372bb8c958470a4b8a1cc88df},children)
)
});
Button_button_95e42ef5e6e269cf3d8d5b17cfe4e82a_4c100443.displayName = "Button";
Actual generated initialState declaration, including framework substates
export const initialState = {"reflex___state____state": {"is_hydrated_rx_state_": false, "router_headers_rx_state_": {"host": "", "origin": "", "upgrade": "", "connection": "", "cookie": "", "pragma": "", "cache_control": "", "user_agent": "", "sec_websocket_version": "", "sec_websocket_key": "", "sec_websocket_extensions": "", "accept_encoding": "", "accept_language": "", "raw_headers": {}}, "router_page_rx_state_": {"host": "", "path": "", "raw_path": "", "full_path": "", "full_raw_path": "", "params": {}}, "router_route_id_rx_state_": "", "router_session_rx_state_": {"client_token": "", "client_ip": "", "session_id": ""}, "router_url_rx_state_": {"scheme": "", "netloc": "", "origin": "", "path": "", "query": "", "query_parameters": {}, "fragment": "", "href": ""}}, "reflex___state____state.flow_lab___flow_lab____lab_state": {"count_rx_state_": 0, "doubled_rx_state_": 0, "greeting_rx_state_": "Hello, Ada!", "name_rx_state_": "Ada", "page_rx_state_": "Not loaded", "visits_rx_state_": 0}, "reflex___state____state.reflex___istate___shared____shared_state_base_internal": {}, "reflex___state____state.reflex___state____frontend_event_exception_state": {}, "reflex___state____state.reflex___state____on_load_internal_state": {}, "reflex___state____state.reflex___state____update_vars_internal_state": {}}Those long names encode the state class and its parent hierarchy. _rx_state_ is this checkout's field marker. They are generated implementation details; write LabState.count in Python rather than hand-writing the browser key.
Use rx.cond and rx.foreach for state-dependent branches and
lists. An ordinary Python if cannot test a visitor's future browser value while
compiling the app. Python can still decide static, build-time structure.
First load has two different “hydrations”
From an empty tab to live state
HTTP brings the interface. The socket brings this session’s data. Follow both trips.
RequestResponseLocal workRequest the document
The browser requests / from the frontend. This is HTTP, before the Reflex state socket exists.
Timeline & playback controls
Browser → Frontend server
HTTP or build output · illustrative representation
GET / HTTP/1.1 Host: localhost:3037
This phase shows an explanatory representation. The recorded Socket.IO frames begin when the backend connection is established.
Illustrative timing, not measured latency. Pause or drag the timeline to inspect any phase. The original source and real captured frames remain below.
Reflex state hydration: synchronize the browser's public state with this client's Python state. These are separate steps with similar names.
State.hydrate, then on_load_internal. Tracked
browser storage values, if set, add an update_vars_internal event between them. This app
has none.What is inside the first HTML? The default React Router config here is ssr: false. Do not assume the Python server renders personalized HTML for
every GET. Production SPA builds can create a static shell/fallback; optional route
prerendering changes build-time HTML. Neither is a per-visitor state lookup. In
development, Vite serves the runtime and modules. The actual initial DOM content depends
on the build/fallback configuration; the generated JS supplies initial state, and the
socket supplies current session data.
The full hydration snapshot has more than the app's fields: the root state includes router URL, route identifier, session details and headers; internal substates may be empty objects. The capture below lets you inspect all of them. Initial false/true hydration changes and on-load updates can arrive in several frames; they are not one magic HTML response.
// Actual runtime entry point (imports omitted here):
startTransition(() => {
hydrateRoot(document, createElement(HydratedRouter));
});Actual initial development HTML response (GET /)
Captured from the local Vite frontend before browser execution. Content-Type: text/html. Development script paths differ from a production build.
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta content="width=device-width, initial-scale=1" name="viewport"/><link rel="modulepreload" href="/@id/__x00__virtual:react-router/browser-manifest"/><link rel="modulepreload" href="/app/entry.client.js"/><link rel="modulepreload" href="/app/root.jsx"/><script>
// Only run in browser environment, not during SSR
if (typeof document !== 'undefined') {
try {
const theme = localStorage.getItem("theme") || 'system';
const systemPreference = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
const resolvedTheme = theme === "system" ? systemPreference : theme;
// Apply theme immediately - blocks until complete
// Use classList to avoid overwriting other classes
document.documentElement.classList.remove("light", "dark");
document.documentElement.classList.add(resolvedTheme);
document.documentElement.style.colorScheme = resolvedTheme;
} catch (e) {
// Fallback to system preference on any error (resolve "system" to actual theme)
const fallbackTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
document.documentElement.classList.remove("light", "dark");
document.documentElement.classList.add(fallbackTheme);
document.documentElement.style.colorScheme = fallbackTheme;
}
}
</script><link href="/styles/__reflex_global_styles.css" rel="stylesheet" type="text/css"/><link data-react-router-critical-css="" rel="stylesheet" href="/@react-router/critical.css?pathname=/"/></head><body><section aria-label="Notifications alt+T" tabindex="-1" aria-live="polite" aria-relevant="additions text" aria-atomic="false" data-react-aria-top-layer="true"></section><div data-is-root-theme="true" data-accent-color="blue" data-gray-color="slate" data-has-background="true" data-panel-background="translucent" data-radius="medium" data-scaling="100%" class="radix-themes css-0"><style data-emotion="css 17rg0dp">.css-17rg0dp{position:fixed;width:100vw;height:0;}</style><div title="Connection Error: " class="css-17rg0dp"></div><script>
console.log(
"💿 Hey developer 👋. You can provide a way better UX than this " +
"when your app is loading JS modules and/or running `clientLoader` " +
"functions. Check out https://reactrouter.com/start/framework/route-module#hydratefallback " +
"for more information."
);
</script></div><script>window.__reactRouterContext = {"basename":"/","future":{"unstable_enableNodeReadableStream":false,"unstable_optimizeDeps":true},"routeDiscovery":{"mode":"initial"},"ssr":false,"isSpaMode":true,"criticalCss":{"rel":"stylesheet","href":"/@react-router/critical.css?pathname=/"}};window.__reactRouterContext.stream = new ReadableStream({start(controller){window.__reactRouterContext.streamController = controller;}}).pipeThrough(new TextEncoderStream());</script><script type="module" async="">import "/@id/__x00__virtual:react-router/inject-hmr-runtime";import "/@id/__x00__virtual:react-router/browser-manifest";
import * as route0 from "/app/root.jsx";
window.__reactRouterRouteModules = {"root":route0};
import("/app/entry.client.js");</script><!--$--><script>window.__reactRouterContext.streamController.enqueue("[{\"_1\":2,\"_3\":-5,\"_4\":-5},\"loaderData\",{},\"actionData\",\"errors\"]\n");</script><!--$--><script>window.__reactRouterContext.streamController.close();</script><!--/$--><!--/$--></body></html>What happens between a click and the socket?
Inside the browser: from DOM click to socket
The browser scheduler runs JavaScript. Reflex’s event queue is an array inside that JavaScript, not another thread.
RequestResponseLocal workReact invokes the generated callback
The browser delivers the interaction and React invokes the button callback. The next local phases describe a call chain, not separate browser tasks.
Timeline & playback controls
Local processing in DOM + React
Local code / state operation
DOM click → React callback(_e) Call stack: generated onClick
This is local processing, not an outgoing Socket.IO payload. Advance to a request or response to inspect a recorded message.
Illustrative timing, not measured latency. Pause or drag the timeline to inspect any phase. The original source and real captured frames remain below.
There really is a Reflex hook called useEventLoop. It wires together the
frontend event queue, socket lifecycle, router information, initial events, and browser
error handling. It returns addEvents and connection errors. It runs as JavaScript
inside React; it is not a separate thread and does not replace the browser’s native event loop.
| Mechanism | What it schedules or stores | Where it lives |
|---|---|---|
| Browser event loop | JavaScript callbacks, tasks/microtasks, and opportunities to update rendering | Browser runtime |
| React event handling | Dispatches a browser interaction to the component’s registered callback | Loaded React code in the browser |
Reflex event_queue | Reflex event objects awaiting frontend dispatch | A module-level JavaScript array in the current document |
| Socket.IO transport | Connection management, encoding and delivery of named messages | Browser client and backend socket implementation |
| Python event processor | Registered backend handler work for the session | Python server process |
1. The browser delivers the click; React calls the handler
A click reaches the browser’s event system. React’s event handling invokes the callback associated with this button. The callback runs on the JavaScript call stack. Synchronous calls execute before other queued callbacks can run. Promise continuations use microtasks; browser rendering happens at rendering opportunities, not automatically after each source line. Long synchronous JavaScript can delay both interaction handling and painting. See MDN’s JavaScript execution model.
Those are browser scheduling rules. Our diagram divides the callback into explanatory phases; it does not mean each box is a separate browser task, thread, or rendered frame. A normal callback can call several Reflex functions in the same execution turn.
2. The generated callback constructs a Reflex event
// Readable equivalent of the generated button callback:
const onClick = (_e) => addEvents([
ReflexEvent(LAB + ".increment", {}, {})
], [_e], {});
// Actual ReflexEvent result for this no-argument action:
{ name: "reflex___state____state.flow_lab___flow_lab____lab_state.increment" } The browser/React event _e and the Reflex event are different objects. The
former describes an interaction and supports browser actions. The latter describes the
application operation to dispatch. The entire DOM event is not serialized. For the name
input, the generated callback extracts _e.target.value into payload.value. For the increment button, no arguments are required and the
empty payload is omitted.
useCallback in the generated component helps preserve the callback reference across
React renders. It does not create a background task or make the Python event handler execute
in JavaScript.
3. addEvents applies frontend event actions
addEvents removes null/undefined events, merges event-action settings,
requests a connection when appropriate, and calls applyEventActions. That
function can invoke preventDefault() or stopPropagation() on the
local interaction, apply temporal/throttle/debounce behavior, and then invoke the queueing
target.
| Action | Effect |
|---|---|
preventDefault | Suppresses the browser’s default action for the event, where applicable. This is distinct from stopping propagation. |
stopPropagation | Stops the interaction propagating to further event handlers through the event system. |
| Throttle | Limits invocation frequency using a keyed frontend throttle helper. |
| Debounce | Defers the target through a keyed debounce helper; subsequent activity can replace the pending invocation. |
| Temporal | Can skip the action when the socket is unavailable instead of reconnecting for it. |
The sample button has no configured debounce/throttle action. Its name input is separately
wrapped by a generated DebounceInput with a 300 ms timeout in the saved frontend.
Component-level input debouncing and Reflex event-action debouncing are separate mechanisms;
neither means the browser pauses its entire event loop.
4. queueEvents puts event objects into an ordinary array
// Frontend module state:
const event_queue = [];
// Simplified from queueEvents:
event_queue.push(...events.filter(e => e !== undefined && e !== null));
await processEvent(resolveSocket(socket), navigate, params);
// A transient teaching snapshot, before processEvent shifts it:
[
{ name: "reflex___state____state.flow_lab___flow_lab____lab_state.increment" }
] This array lives in JavaScript memory. It is not the browser’s task queue, Python’s event queue, sessionStorage, a database, or a durable message log. When the connection is ready, dispatch may begin immediately in the same call chain; you should not expect the intermediate array contents to persist long enough to produce a visible UI frame.
prepend=true puts supplied events ahead of pending ones. The runtime uses it
for initialization/reconnection work. If the queue contains stateful work and the socket
is unavailable, processEvent returns before removing the next event. Connection
setup later drains pending events. Refresh discards this in-memory array; it is not an offline-delivery
guarantee.
5. processEvent selects local, REST, or socket handling
// Readable outline; inspect the source excerpt below for the real code:
if (statefulWorkQueued && !socket.connected) return;
const event = event_queue.shift();
if (event.handler) {
await applyRestEvent(event, ...);
} else {
await applyEvent(event, ...);
} applyEvent recognizes browser-side commands such as redirects and script
execution. For an ordinary Python handler event, it attaches router_data if
missing, then calls socket.emit("event", event). The browser token belongs to
the connection in this checkout. REST/upload paths are separate from this button’s socket
path.
“Queue drained” does not mean “Python finished.” The normal socket path
awaits local dispatch code, not the server’s state delta. There is no final=true gate here. The inspected processEvent implementation removes the next event and
continues local processing; it does not contain a single-flight “wait for the backend reply”
flag. Separate asynchronous entry points can interleave at await boundaries, so do not infer
global server-completion ordering from this frontend array.
6. JavaScript continues while Python works
Socket transmission does not make the callback synchronously execute the Python handler.
After local JavaScript yields or returns, the browser can handle other work. Later,
incoming network data causes Socket.IO to invoke its registered "event" listener.
That listener is a different entry into the frontend runtime from the original click.
Click callback → outgoing queue → socket.emit()
│
Python processes event
│
Later socket callback ← StateUpdate.delta
↓
React reducer dispatch → updated context → render/commit 7. The incoming listener updates React state
The incoming callback validates substate names, dispatches each received substate delta to its reducer, applies configured client-storage changes, and queues any returned frontend events. The reducer shallow-merges the changed fields. React then schedules/render-commits the affected UI; the callback itself does not assign the counter’s HTML directly.
This receive path can report a fatal frontend/backend state-definition mismatch if a delta
targets an unknown substate. The current outgoing processing code also drops pending
events after such a mismatch to avoid repeatedly processing an invalid queue. Connection
errors and frontend exception reporting are other responsibilities wired through useEventLoop.
How to inspect it in DevTools
- Open Sources and find the generated frontend’s
utils/state.js. In a production build, source maps and minification settings affect how easily this maps back to source; development mode is simpler. - Break in the generated button callback, then
addEvents,queueEvents,processEvent, and just beforesocket.emit. Inspect_e, the separate Reflex event, and the module’sevent_queuein scope. - Break separately in
socket.current.on("event", ...)and inspect the incoming update. Notice that the original click callback is no longer waiting on the stack for this reply. - Break at
applyDeltato compare the old substate with the changed fields. Use Network → WebSocket messages to compare the real request and response.
Frontend source inspected at checkout 35c11e12c: packages/reflex-base/src/reflex_base/.templates/web/utils/state.js. The
diagram’s network phases reuse the guide’s saved counter request/response. Its queue
snapshots and local call-stack phases are explanatory, not a browser performance trace.
Actual queueEvents and processEvent source
export const queueEvents = async (
events,
socket,
prepend,
navigate,
params,
) => {
if (prepend) {
// Drain the existing queue and place it after the given events.
events = [
...events,
...Array.from({ length: event_queue.length }).map(() =>
event_queue.shift(),
),
];
}
event_queue.push(...events.filter((e) => e !== undefined && e !== null));
await processEvent(resolveSocket(socket), navigate, params);
};
/**
* Process an event off the event queue.
* @param socket The socket object to send the event on.
* @param navigate The navigate function from React Router
* @param params The params object from React Router
*/
export const processEvent = async (socket, navigate, params) => {
// Only proceed if the socket is up or no event in the queue uses state, otherwise we throw the event into the void
if (isStateful() && !(socket && socket.connected)) {
return;
}
// A backend/frontend state mismatch is fatal; do not send further events.
// Drop pending events too: callers drain the queue in while-loops that
// would otherwise spin forever on an early return.
if (backend_state_mismatch) {
event_queue.length = 0;
return;
}
// Only proceed if we're not already processing an event.
if (event_queue.length === 0) {
return;
}
// Apply the next event in the queue.
const event = event_queue.shift();
// Process events with handlers via REST and all others via websockets.
if (event.handler) {
await applyRestEvent(event, socket, navigate, params);
} else {
await applyEvent(event, socket, navigate, params);
}
// Process any remaining events.
if (event_queue.length > 0) {
await processEvent(socket, navigate, params);
}
};
Actual applyEventActions source
export const applyEventActions = (
target,
event_actions = {},
args = [],
action_key = null,
temporal_handler = null,
) => {
if (!(args instanceof Array)) {
args = [args];
}
const _e = args.find((o) => o?.preventDefault !== undefined);
if (event_actions?.preventDefault && _e?.preventDefault) {
_e.preventDefault();
}
if (event_actions?.stopPropagation && _e?.stopPropagation) {
_e.stopPropagation();
}
if (event_actions?.temporal && temporal_handler && !temporal_handler()) {
return;
}
const invokeTarget = () => target(...args);
const resolved_action_key = action_key ?? target.toString();
if (event_actions?.throttle) {
if (!throttle(resolved_action_key, event_actions.throttle)) {
return;
}
}
if (event_actions?.debounce) {
debounce(resolved_action_key, invokeTarget, event_actions.debounce);
return;
}
return invokeTarget();
};
Follow one click, one layer at a time
A click is a round trip, not new HTML
Watch count stay at 0 in the browser until the server’s delta arrives.
RequestResponseLocal workClick Add one
The generated React callback builds a named event. It does not send count=1 or the complete DOM click object.
Timeline & playback controls
Local processing in Browser
Local code / state operation
on_click=LabState.increment → ReflexEvent(LAB + ".increment")
This is local processing, not an outgoing Socket.IO payload. Advance to a request or response to inspect a recorded message.
Illustrative timing, not measured latency. Pause or drag the timeline to inspect any phase. The original source and real captured frames remain below.
This deterministic simulation starts after the Home page has hydrated. Choose an action and step through it. Each action begins from the same baseline so you can compare them. The packet shapes match the captured app; only the relevant app substate is shown.
The screen is ready
Both sides agree after hydration. The browser holds public fields; Python also holds _handled.
Browser's app state
Python's app state
Selected layer's data
What exactly crosses the boundary?
| Where | Original form | What it means |
|---|---|---|
| Python declaration | count: int = 0 | Typed state field and default. A class reference becomes a Var expression when used in a component. |
| Python session instance | self.count == 1 | Actual Python integer for this token. |
| Browser DOM event | event.target.value === "Lin" | JavaScript string extracted from the input. The whole DOM/React event object is not sent. |
| Browser event object | {name: LAB + ".rename", payload: {value: "Lin"}, router_data:
…} | Handler identifier, named arguments, and browser routing information. |
| Socket.IO frame | 42/_event,["event",{…}] | Engine.IO message type 4 + Socket.IO EVENT type 2 + namespace + JSON array. |
| Python deserialization | Event(name=…, payload={"value": "Lin"}, router_data=…) | Socket.IO decodes JSON to a dict. EventNamespace builds an Event and enriches routing metadata. Defaults supply omitted fields. |
| Python handler call | rename(value="Lin") | The event argument binds to the annotated parameter; self is the resolved session state. |
| Python update object | StateUpdate(delta={LAB: {"name_rx_state_": "Lin",
"greeting_rx_state_": "Hello, Lin!"}}) | Dirty public fields plus affected computed fields. |
| Browser state after merge | {…oldState, …delta[LAB]} | JavaScript strings/numbers, then React text nodes and DOM changes. |
LAB in shortened explanations means reflex___state____state.flow_lab___flow_lab____lab_state. Actual captures use
the full name. Object key order has no semantic meaning.
Actual rename request payload
{
"name": "reflex___state____state.flow_lab___flow_lab____lab_state.rename",
"payload": {"value": "Lin"},
"router_data": {"pathname": "/", "asPath": "/"}
}Actual rename response payload
{
"delta": {
"reflex___state____state.flow_lab___flow_lab____lab_state": {
"name_rx_state_": "Lin",
"greeting_rx_state_": "Hello, Lin!"
}
}
}No token in this normal event body. This checkout sends the token when
connecting. The backend resolves it from the socket SID. increment also omits
an empty payload. The StateUpdate serializer omits empty fields, so these responses have
no empty events list and no final: true.
Raw text frame, before Socket.IO unwraps it
42/_event,["event",{"name":"reflex___state____state.flow_lab___flow_lab____lab_state.rename","payload":{"value":"Lin"},"router_data":{"pathname":"/","asPath":"/"}}] This is WebSocket application frame text, not the TCP/TLS bytes. The 42 prefix is protocol framing, not a count. The namespace is /_event; the named
Socket.IO event is "event". They are different things. Engine.IO handshake
and heartbeat packets are additional traffic. The configured default transport is
WebSocket, not a mandatory polling-then-upgrade sequence; polling can be configured
separately. In deployment, HTTPS/WSS adds transport encryption without changing this
logical payload.
Python types do not travel as Python objects
For this app: Python int becomes a JSON number and then a JS number; str becomes a JSON/JS string; bool becomes JSON true/false; None becomes
null. Lists become arrays, mappings become objects after supported serialization. No pickle
or executable Python is sent to the browser. More complex values require Reflex serializers;
dates, models, custom classes, and large numbers should be checked against their specific serializer
and JavaScript representation. Python annotations alone do not guarantee a lossless network
representation.
A JSON dump stored inside a JSON capture file has escaped quotes such as \"name\". That outer escaping belongs to the capture file's string; the actual WebSocket
text above has ordinary quotes.
Read the real conversation
Captured with headless Chromium against the running app: initial Home load → increment →
rename to Lin → Details → refresh. These are actual /_event frames, not generated
by the simulation. Volatile client IDs and WebSocket keys are replaced with labeled placeholders.
Vite's separate hot-reload connection is excluded. Expand the complete transcript for a no-JavaScript
view.
WebSocket text
Decoded event or handshake
Complete sanitized frame transcript
[
{
"direction": "server → browser",
"data": "0{\"sid\":\"<SOCKET_ID_1>\",\"upgrades\":[],\"pingTimeout\":120000,\"pingInterval\":25000,\"maxPayload\":1000000}"
},
{
"direction": "browser → server",
"data": "40/_event,"
},
{
"direction": "server → browser",
"data": "40/_event,{\"sid\":\"<SOCKET_ID_2>\"}"
},
{
"direction": "browser → server",
"data": "42/_event,[\"event\",{\"name\":\"reflex___state____state.hydrate\",\"router_data\":{\"pathname\":\"/\",\"asPath\":\"/\"}}]"
},
{
"direction": "browser → server",
"data": "42/_event,[\"event\",{\"name\":\"reflex___state____state.reflex___state____on_load_internal_state.on_load_internal\",\"router_data\":{\"pathname\":\"/\",\"asPath\":\"/\"}}]"
},
{
"direction": "server → browser",
"data": "42/_event,[\"event\",{\"delta\":{\"reflex___state____state\":{\"is_hydrated_rx_state_\":false,\"router_headers_rx_state_\":{\"host\":\"localhost:8037\",\"origin\":\"http://localhost:3037\",\"upgrade\":\"websocket\",\"connection\":\"Upgrade\",\"cookie\":\"\",\"pragma\":\"no-cache\",\"cache_control\":\"no-cache\",\"user_agent\":\"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.7922.34 Safari/537.36\",\"sec_websocket_version\":\"13\",\"sec_websocket_key\":\"<SOCKET_KEY_3>\",\"sec_websocket_extensions\":\"permessage-deflate; client_max_window_bits\",\"accept_encoding\":\"gzip, deflate, br, zstd\",\"accept_language\":\"\",\"raw_headers\":{\"accept-encoding\":\"gzip, deflate, br, zstd\",\"asgi-scope-client\":\"127.0.0.1\",\"cache-control\":\"no-cache\",\"connection\":\"Upgrade\",\"host\":\"localhost:8037\",\"origin\":\"http://localhost:3037\",\"pragma\":\"no-cache\",\"sec-websocket-extensions\":\"permessage-deflate; client_max_window_bits\",\"sec-websocket-key\":\"<SOCKET_KEY_3>\",\"sec-websocket-protocol\":\"0.9.10.post41.dev0+9c270ca6c\",\"sec-websocket-version\":\"13\",\"upgrade\":\"websocket\",\"user-agent\":\"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.7922.34 Safari/537.36\"}},\"router_page_rx_state_\":{\"host\":\"http://localhost:3037\",\"path\":\"/index\",\"raw_path\":\"/\",\"full_path\":\"http://localhost:3037/index\",\"full_raw_path\":\"http://localhost:3037/\",\"params\":{}},\"router_route_id_rx_state_\":\"/index\",\"router_session_rx_state_\":{\"client_token\":\"<CLIENT_TOKEN_4>\",\"client_ip\":\"127.0.0.1\",\"session_id\":\"<SOCKET_ID_2>\"},\"router_url_rx_state_\":{\"scheme\":\"http\",\"netloc\":\"localhost:3037\",\"origin\":\"http://localhost:3037\",\"path\":\"/\",\"query\":\"\",\"query_parameters\":{},\"fragment\":\"\",\"href\":\"http://localhost:3037/\"}},\"reflex___state____state.reflex___state____on_load_internal_state\":{},\"reflex___state____state.flow_lab___flow_lab____lab_state\":{\"count_rx_state_\":0,\"doubled_rx_state_\":0,\"greeting_rx_state_\":\"Hello, Ada!\",\"name_rx_state_\":\"Ada\",\"page_rx_state_\":\"Not loaded\",\"visits_rx_state_\":0},\"reflex___state____state.reflex___istate___shared____shared_state_base_internal\":{},\"reflex___state____state.reflex___state____frontend_event_exception_state\":{},\"reflex___state____state.reflex___state____update_vars_internal_state\":{}}}]"
},
{
"direction": "server → browser",
"data": "42/_event,[\"event\",{\"delta\":{\"reflex___state____state\":{\"is_hydrated_rx_state_\":false}}}]"
},
{
"direction": "server → browser",
"data": "42/_event,[\"event\",{\"delta\":{\"reflex___state____state.flow_lab___flow_lab____lab_state\":{\"page_rx_state_\":\"Home\",\"visits_rx_state_\":1}}}]"
},
{
"direction": "server → browser",
"data": "42/_event,[\"event\",{\"delta\":{\"reflex___state____state\":{\"is_hydrated_rx_state_\":true}}}]"
},
{
"direction": "browser → server",
"data": "42/_event,[\"event\",{\"name\":\"reflex___state____state.flow_lab___flow_lab____lab_state.increment\",\"router_data\":{\"pathname\":\"/\",\"asPath\":\"/\"}}]"
},
{
"direction": "server → browser",
"data": "42/_event,[\"event\",{\"delta\":{\"reflex___state____state.flow_lab___flow_lab____lab_state\":{\"doubled_rx_state_\":2,\"count_rx_state_\":1}}}]"
},
{
"direction": "browser → server",
"data": "42/_event,[\"event\",{\"name\":\"reflex___state____state.flow_lab___flow_lab____lab_state.rename\",\"payload\":{\"value\":\"Lin\"},\"router_data\":{\"pathname\":\"/\",\"asPath\":\"/\"}}]"
},
{
"direction": "server → browser",
"data": "42/_event,[\"event\",{\"delta\":{\"reflex___state____state.flow_lab___flow_lab____lab_state\":{\"name_rx_state_\":\"Lin\",\"greeting_rx_state_\":\"Hello, Lin!\"}}}]"
},
{
"direction": "browser → server",
"data": "42/_event,[\"event\",{\"name\":\"reflex___state____state.reflex___state____on_load_internal_state.on_load_internal\",\"router_data\":{\"pathname\":\"/details\",\"asPath\":\"/details\"}}]"
},
{
"direction": "server → browser",
"data": "42/_event,[\"event\",{\"delta\":{\"reflex___state____state\":{\"is_hydrated_rx_state_\":false,\"router_route_id_rx_state_\":\"/details\",\"router_page_rx_state_\":{\"host\":\"http://localhost:3037\",\"path\":\"/details\",\"raw_path\":\"/details\",\"full_path\":\"http://localhost:3037/details\",\"full_raw_path\":\"http://localhost:3037/details\",\"params\":{}},\"router_url_rx_state_\":{\"scheme\":\"http\",\"netloc\":\"localhost:3037\",\"origin\":\"http://localhost:3037\",\"path\":\"/details\",\"query\":\"\",\"query_parameters\":{},\"fragment\":\"\",\"href\":\"http://localhost:3037/details\"}}}}]"
},
{
"direction": "server → browser",
"data": "42/_event,[\"event\",{\"delta\":{\"reflex___state____state.flow_lab___flow_lab____lab_state\":{\"page_rx_state_\":\"Details\",\"visits_rx_state_\":2}}}]"
},
{
"direction": "server → browser",
"data": "42/_event,[\"event\",{\"delta\":{\"reflex___state____state\":{\"is_hydrated_rx_state_\":true}}}]"
},
{
"direction": "browser → server",
"data": "41/_event,"
},
{
"direction": "server → browser",
"data": "0{\"sid\":\"<SOCKET_ID_5>\",\"upgrades\":[],\"pingTimeout\":120000,\"pingInterval\":25000,\"maxPayload\":1000000}"
},
{
"direction": "browser → server",
"data": "40/_event,"
},
{
"direction": "server → browser",
"data": "40/_event,{\"sid\":\"<SOCKET_ID_6>\"}"
},
{
"direction": "browser → server",
"data": "42/_event,[\"event\",{\"name\":\"reflex___state____state.hydrate\",\"router_data\":{\"pathname\":\"/details\",\"asPath\":\"/details\"}}]"
},
{
"direction": "browser → server",
"data": "42/_event,[\"event\",{\"name\":\"reflex___state____state.reflex___state____on_load_internal_state.on_load_internal\",\"router_data\":{\"pathname\":\"/details\",\"asPath\":\"/details\"}}]"
},
{
"direction": "server → browser",
"data": "42/_event,[\"event\",{\"delta\":{\"reflex___state____state\":{\"is_hydrated_rx_state_\":false,\"router_headers_rx_state_\":{\"host\":\"localhost:8037\",\"origin\":\"http://localhost:3037\",\"upgrade\":\"websocket\",\"connection\":\"Upgrade\",\"cookie\":\"\",\"pragma\":\"no-cache\",\"cache_control\":\"no-cache\",\"user_agent\":\"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.7922.34 Safari/537.36\",\"sec_websocket_version\":\"13\",\"sec_websocket_key\":\"<SOCKET_KEY_7>\",\"sec_websocket_extensions\":\"permessage-deflate; client_max_window_bits\",\"accept_encoding\":\"gzip, deflate, br, zstd\",\"accept_language\":\"\",\"raw_headers\":{\"accept-encoding\":\"gzip, deflate, br, zstd\",\"asgi-scope-client\":\"127.0.0.1\",\"cache-control\":\"no-cache\",\"connection\":\"Upgrade\",\"host\":\"localhost:8037\",\"origin\":\"http://localhost:3037\",\"pragma\":\"no-cache\",\"sec-websocket-extensions\":\"permessage-deflate; client_max_window_bits\",\"sec-websocket-key\":\"<SOCKET_KEY_7>\",\"sec-websocket-protocol\":\"0.9.10.post41.dev0+9c270ca6c\",\"sec-websocket-version\":\"13\",\"upgrade\":\"websocket\",\"user-agent\":\"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.7922.34 Safari/537.36\"}},\"router_page_rx_state_\":{\"host\":\"http://localhost:3037\",\"path\":\"/details\",\"raw_path\":\"/details\",\"full_path\":\"http://localhost:3037/details\",\"full_raw_path\":\"http://localhost:3037/details\",\"params\":{}},\"router_route_id_rx_state_\":\"/details\",\"router_session_rx_state_\":{\"client_token\":\"<CLIENT_TOKEN_4>\",\"client_ip\":\"127.0.0.1\",\"session_id\":\"<SOCKET_ID_6>\"},\"router_url_rx_state_\":{\"scheme\":\"http\",\"netloc\":\"localhost:3037\",\"origin\":\"http://localhost:3037\",\"path\":\"/details\",\"query\":\"\",\"query_parameters\":{},\"fragment\":\"\",\"href\":\"http://localhost:3037/details\"}},\"reflex___state____state.reflex___state____on_load_internal_state\":{},\"reflex___state____state.flow_lab___flow_lab____lab_state\":{\"count_rx_state_\":1,\"doubled_rx_state_\":2,\"greeting_rx_state_\":\"Hello, Lin!\",\"name_rx_state_\":\"Lin\",\"page_rx_state_\":\"Details\",\"visits_rx_state_\":2},\"reflex___state____state.reflex___istate___shared____shared_state_base_internal\":{},\"reflex___state____state.reflex___state____frontend_event_exception_state\":{},\"reflex___state____state.reflex___state____update_vars_internal_state\":{}}}]"
},
{
"direction": "server → browser",
"data": "42/_event,[\"event\",{\"delta\":{\"reflex___state____state\":{\"is_hydrated_rx_state_\":false}}}]"
},
{
"direction": "server → browser",
"data": "42/_event,[\"event\",{\"delta\":{\"reflex___state____state.flow_lab___flow_lab____lab_state\":{\"page_rx_state_\":\"Details\",\"visits_rx_state_\":3}}}]"
},
{
"direction": "server → browser",
"data": "42/_event,[\"event\",{\"delta\":{\"reflex___state____state\":{\"is_hydrated_rx_state_\":true}}}]"
}
]The first hydrate sends count 0/name Ada. Increment sends only count and doubled. Rename sends only name and greeting. Navigation sends router changes, page/visits, and hydration status. After refresh, a new connection hydrates count 1/name Lin from the retained session, then Details on-load increments visits from 2 to 3.
From event name to Python mutation
- Connection context:
EventNamespace.on_eventfinds the token associated with the SID. An event from an unassociated socket is not processed as a normal session event. - Decode and enrich: parse the incoming mapping into an Event, merge connection metadata such as headers/IP, normalize query data, resolve the route, and attach server-known client identity.
- Queue:
event_processor.enqueue(token, event)schedules the registered handler. This checkout has backend event queues and event futures. It is not the older “browser waits for final=true” protocol. - Resolve state and lock: the BaseState event processor obtains the relevant state through the state manager. Foreground state handlers run with the modification context protecting that client's state. Different tokens can be processed independently; this is not a single global counter shared by all visitors.
- Middleware and handler: update route variables, allow preprocessing,
resolve the event target and arguments, and call the handler.
self.count += 1updates the actual session integer;_handled += 1updates backend-only bookkeeping. - Dirty tracking: state assignment marks fields dirty. Computed
dependency tracking invalidates
doubledwhen count changes.get_delta()collects dirty public base variables and computed variables, adds the field marker, and recurses through dirty substates. - Resolve and emit: compute/resolve delta values, apply the relevant postprocessing, and emit StateUpdate to the socket belonging to that token. Dirty tracking is cleaned at the framework's flush boundaries. The state manager retains/persists state under its chosen policy.
Why doesn't _handled appear? Underscore backend variables are
excluded from the public state delta. The app still sends the visible effect, such as the new
count. Excluding a field does not protect values you explicitly return through other public
fields or responses. Keep secrets out of public state and build-time defaults.
Why does a computed field change without an assignment? @rx.var records a dependency on the fields read by its Python getter. A changed dependency invalidates
the cached computation. The new result is sent as data. JavaScript is not executing the Python doubled getter.
The sample contains simple atomic handlers. A real database write belongs in server-side logic but has its own database transaction: the Reflex state lock is not a database transaction, nor is a state manager a substitute for permanent application storage.
A delta changes state; React changes the DOM
// The runtime reducer in this checkout:
export const applyDelta = (state, delta) => {
return { ...state, ...delta };
};
// Conceptual call for the counter reply:
dispatch[LAB]({ count_rx_state_: 1, doubled_rx_state_: 2 }); The socket callback verifies that the delta's substate names have registered dispatch functions. It dispatches each substate update into its reducer, synchronizes configured browser-storage fields, and handles any frontend command events. An unknown substate signals a stale/mismatched frontend/backend build and is reported as a client error.
The merge is shallow at the substate field level. Omitted fields keep their previous values. A changed list/dict field is generally sent as its serialized field value here; this is not an arbitrary JSON Patch protocol and does not carry instructions such as “insert DOM node 7”. React consumes the updated context, evaluates affected components, reconciles elements, and commits the required DOM updates. The browser then performs any necessary style/layout/paint work.
For the counter button, Python decides the new value; the screen waits for the server
delta. For an input, the browser naturally has local keystroke behavior, and Reflex's
input components can manage/debounce that local value before sending updates. Do not infer
that every visible keystroke waits for a server response, or that every key generates
exactly one packet. The captured test filled the input with Lin and observed one
rename event.
Changing state is not recompilation. The page function, stylesheet generation, and JS bundling do not repeat for each counter event. In development, editing source code may trigger recompilation/hot reload; Vite's development WebSocket is separate from the Reflex state socket.
What happens when you click Details?
New page. Same counter. Same connection.
This sequence begins after one click: count is 1 and the Home page has one visit.
RequestResponseLocal workFollow the internal link
The app link requests client-side navigation to /details. This is a local browser action.
Timeline & playback controls
Browser → React Router
Local code / state operation
rx.link("Details", href="/details") This is local processing, not an outgoing Socket.IO payload. Advance to a request or response to inspect a recorded message.
Illustrative timing, not measured latency. Pause or drag the timeline to inspect any phase. The original source and real captured frames remain below.
- The client router handles a normal same-app link. It updates browser history/location and selects the route component. Route modules/assets may be fetched if not already available.
- The event-loop navigation effect marks the local root state as not hydrated and queues on-load processing with the new router data.
- The server normalizes the route and updates router state. It selects the registered
enter_detailshandler, followed by setting is_hydrated true. - The server emits router and page/visits deltas. Count and name are unchanged and remain available to the Details component. The new page can render before its on-load work completes; use hydration/loading state when that matters.
| Action | Document / connection | State / on-load |
|---|---|---|
| Home → Details link | Client navigation; normally same document and socket. | No ordinary full hydrate. on_load_internal runs; same LabState. |
| Refresh /details | New document and connection; generated defaults are initialized again. | Token usually survives in the same tab's sessionStorage. Full hydrate restores retained state, then on-load runs again. |
| Direct visit /details | Frontend host must serve the route/SPA fallback, then load route JS. | Hydrate first; Details on-load then runs. A new token gets defaults. |
| Back / forward | Normally React Router history navigation within the live app; a full-document history restore can involve browser page cache/reconnection. | Normal route navigation invokes on-load; count/name remain in the token's state. |
| Query change | Same route can receive different search data. | The navigation effect checks pathname + search. Query data accompanies the event. Example: /details?tab=activity. |
| Hash-only anchor | Often scrolls inside the document. | The current effect skips on-load when pathname/search match and the location has a hash. Do not assume every URL character change triggers on-load. |
| External link | Leaves the app or opens another tab/document. | No ordinary same-app on-load round trip. The guide link opens a standalone document. |
Query and dynamic-route example (not a third page in this app)
// /details?tab=activity#top, on a later event:
"router_data": {
"pathname": "/details",
"asPath": "/details?tab=activity#top",
"query": {"tab": "activity"}
} The browser extracts the query string and merges dynamic route parameters. A route such as /items/[item_id] has a route pattern and a concrete URL; the server uses both to resolve the page and expose
router information. Browser query values begin as strings, not trusted typed database identifiers.
Validate and authorize them in server logic. This simple query extraction uses Object.fromEntries,
so repeated query keys should not be assumed to become lists automatically.
Rapid navigation can supersede an unfinished prior on-load chain: this checkout marks on_load_internal with supersedes=True. That helps avoid stale page-load work winning the race.
It cannot undo external side effects already completed by your handler.
Browser copy, session state, and durable records are different
Why initialState contains more than your counter
Follow build-time placeholders as they become session-specific values. Only public fields cross to React.
RequestResponseLocal workShip the default state shape
The compiler emits both the root and app substates. It knows Ada and count=0, but not this visitor’s headers, URL, token, or SID.
Timeline & playback controls
Compiler → Browser contexts
HTTP or build output · illustrative representation
{
"root": {
"is_hydrated": false,
"router_headers": {
"host": ""
},
"router_session": {
"client_token": "",
"session_id": ""
}
},
"LabState": {
"name": "Ada",
"count": 0
}
} This phase shows an explanatory representation. The recorded Socket.IO frames begin when the backend connection is established.
Illustrative timing, not measured latency. Pause or drag the timeline to inspect any phase. Shortened field names in this figure; full generated names remain in the source and captures.
Browser
- DOM: visible output and native input state.
- React contexts: a copy of public state fields.
- sessionStorage: this runtime's client token; survives refresh in the tab.
- Optional configured Cookie / LocalStorage / SessionStorage fields.
Server
- State manager: authoritative Python session state.
- Backend-only variables and event handlers.
- Optional Redis coordination and persistence policies.
- Optional database: durable domain records under your app's control.
The default configured state-manager mode in this checkout is disk. Memory and Redis implementations also exist; configuring Redis can select Redis when the mode was not explicitly set. Storage lifetime, eviction, expiration, and restarts affect whether an existing token finds existing state. Refresh retaining the count in our capture demonstrates this run, not an unconditional durability guarantee.
Two independent browser sessions generally have separate tokens and counts. Duplicating a
tab can copy browser sessionStorage, so this implementation also detects duplicate tokens
and can send new_token. A token identifies a UI session; it is not proof of a
user's identity or authorization. Shared-state features are explicit exceptions to the
ordinary per-token model.
For fields declared with browser storage, generated code reads configured values and sends update_vars_internal on initial load/navigation when values exist. The server applies those values, and subsequent
deltas can update storage. This example intentionally uses ordinary state fields so you can
see what the session manager alone does. React context itself disappears on a full page reload.
Public state is inspectable by the visitor. A field does not become private merely because no component displays it. Public snapshots may include fields used elsewhere. Use backend-only fields for server-only data, and enforce authorization inside handlers before performing protected work.
Where the other framework pieces fit
These are extensions of the model, not behaviors all exercised by this app. Follow their source/docs when using them; the captured packets above only prove the sample's path.
| Feature | Where it fits | What changes in the flow |
|---|---|---|
| Yield / returned events | Server event processing | A handler can chain registered server handlers or request frontend actions. A yield can flush current state before continuing; several updates can belong to one action. |
| Background tasks | Server scheduler | Long-lived work can run outside the ordinary locked foreground handler. Use the framework's state modification context, typically async with self, for protected writes. A Python async function alone does not make blocking work nonblocking. |
| Client-only actions | Browser event dispatcher | Redirects, scripts, focus, and similar frontend commands can run locally. Not every Reflex event must invoke Python. A server handler can request these commands too. |
| Forms | DOM → event payload | Submit gathers named input values into a payload. A form can send several fields together instead of one input change at a time. Handler validation still matters. |
| Uploads | HTTP endpoint + event processing | Files use multipart HTTP/upload handling rather than squeezing file objects into ordinary JSON socket events. The runtime also has REST event paths; don't generalize the counter's transport to every API. |
| Middleware / error handling | Around event execution and updates | Can inspect/alter processing or return updates; configured exception handlers determine recovery. A failing handler is not evidence its external side effects were rolled back. |
| rx.Model / database integration | Python business logic | Query/commit database rows in the server. Copy safe result data into public state to show it. Database contents are not automatically sent just because a model exists. |
| Components / props / style | Compile + React runtime | Python wrappers declare React components, imports, event triggers, and prop serialization. Styles and responsive rules become browser CSS/props. Third-party components can have additional internal JS state. |
| Custom serializers / custom JS | Boundary adapters | Translate supported Python data into frontend values or wrap custom browser components. Verify both type and runtime representation rather than assuming arbitrary Python objects can cross. |
| Static / stateless pages | Compiler + frontend | A fully stateless app can avoid a state socket. It still needs its browser assets. The stateful app in this guide does need the Python backend. |
| Prerender / SEO / metadata | Build and document generation | Route metadata and prerender options affect initial HTML and crawlers. They do not make a build-time snapshot personalized to a future visitor. |
| Deployment / proxies / scaling | Serving and state infrastructure | Configure frontend/API origins, asset paths, and WebSocket forwarding. Multiple backend instances need appropriate state/socket coordination; independent memory state is not shared across processes. |
| Reconnect / offline | Browser event loop + socket | The runtime attempts reconnection and rehydration. Do not treat a broken connection as an exactly-once delivery guarantee; make important side effects retry-safe where your application requires it. |
Turn the browser into your packet viewer
- Run the app, open the browser developer tools, and select Network. Enable Preserve log, then refresh.
- Inspect the document request for
/. Its Response is actual HTML. Compare View Source with the Elements panel: Elements shows the DOM after JavaScript may have changed it. - Inspect JavaScript/CSS requests. In development, modules and the Vite runtime appear separately; production bundles look different.
- Filter to WebSocket. Select the backend
/_eventconnection, not Vite's hot-reload socket. Its URL includes the token query and Engine.IO parameters. In this local example, the backend port is 8037. - Open Messages/Frames. Find hydrate and on_load_internal; then click Add one. Compare the request without a payload to the reply with count and doubled.
- Change the name to Lin. The request payload contains a string under value. The reply contains name and greeting; there is no Python source and no full HTML page.
- Click Details. Watch pathname/asPath, router deltas, visits, and is_hydrated. Confirm the count remains.
- Refresh on Details. See a new connection and a full snapshot of the retained session. Compare this to opening a fresh independent browser context, which starts from defaults.
applyEvent before socket.emit; EventNamespace.on_event after Event creation; LabState.increment; BaseState.get_delta;
the socket.on("event") callback; and applyDelta.Common wrong mental models
- “Python runs in the browser.” This compiled app runs JavaScript/React in the browser and Python on the server.
- “The server sends HTML for every click.” The counter's handler sends a state delta; React handles the DOM.
- “The browser sends the new counter.” The button sends a handler name. Python computes the increment from server state.
- “All fields are sent every time.” Hydration sends a full public snapshot; normal changes usually send dirty fields plus dependent computed values.
- “A route is a new Python state.” Both pages share the token's LabState. Page on-load is a separate event chain.
- “Hydration means the user is authenticated.” It means state synchronization/on-load completed; authentication is separate.
A map back to the repository
Paths are relative to the repository root. Symbol names are more durable than line numbers. The guide reflects the checked-out revision at creation; generated names and event scheduling may change in future versions.
examples/flow_lab/flow_lab/flow_lab.py— all sample handlers and pages.reflex/app.py— App.add_page, App._compile, EventNamespace.on_connect / on_event / emit_update / link_token_to_sid.reflex/compiler/compiler.py— compile_app, _compile_page, _compile_contexts.reflex/compiler/utils.py— compile_state, serialization of compiler defaults.packages/reflex-base/src/reflex_base/compiler/templates.py— generated contexts, initialEvents, onLoadInternalEvent.packages/reflex-base/src/reflex_base/.templates/web/utils/state.js— getToken, ReflexEvent, applyEvent, connect, useEventLoop, applyDelta, client-storage synchronization, navigation effect.packages/reflex-base/src/reflex_base/.templates/web/app/entry.client.js— React hydrateRoot entry point.packages/reflex-base/src/reflex_base/.templates/web/react-router.config.js— default ssr:false.reflex/state.py— BaseState.dict / get_delta, State.hydrate, OnLoadInternalState.on_load_internal, StateUpdate and serialize_state_update.packages/reflex-base/src/reflex_base/event/processor/event_processor.py— queueing, event futures, emitted updates, superseding chains.packages/reflex-base/src/reflex_base/event/processor/base_state_processor.py— state resolution, locking, middleware, handler execution, yielded deltas.reflex/istate/manager/{__init__,memory,disk,redis}.py— session state backends.packages/reflex-base/src/reflex_base/config.py— transport and state-manager defaults.
The legacy HydrateMiddleware implementation also exists in the repository, but this guide follows the current registered State.hydrate event and the actual captured runtime. Finding a file named “hydrate” is not enough to establish which code path the app uses.