September 13, 2026   ·   Engineering

What actually happens
when you click?

How Reflex turns Python into a React interface, and moves data between your browser and server.

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.

01 / Start small

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],
)
02 / Two runtimes, two kinds of work

Python makes the instructions. React runs the screen.

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.

03 / Before the visitor arrives

How your Python becomes browser code

Figure 05 · Follow the data

From Python objects to a runnable browser app

Follow the artifacts. UI declarations produce React modules; Python event handlers stay on the backend.

RequestResponseLocal work
From Python objects to a runnable browser app
01 · Local processing

Execute the page-building code

Python calls component constructors. This creates objects describing the interface, not HTML in a browser.

Timeline & playback controls
1 / 8
Compilation artifact inspector

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.

Reflex is a UI compiler, not a general Python-to-JavaScript translator. Python executes your page function to create component objects. Reflex generates React modules from those objects. The frontend toolchain resolves and bundles those modules. Your Python state class and event methods remain Python on the backend.

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 → DOM

Backend 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 delta

1. 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?

LocationConcrete formLifetime / purpose
Application sourceflow_lab/flow_lab.py, rxconfig.pyAuthored Python on disk. Imported by compilation and backend startup.
Compiler process memoryPython component objects, Var expressions, registries, render dictionaries, generated source stringsBuild-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 modulesInput to the frontend toolchain. Generated files should be changed through Python source, not hand-edited as the source of truth.
.web/utils/context.jsJavaScript defaults, contexts, reducers, and event setupInitial 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 dependenciesBuild/development dependencies. The browser receives the needed transformed/bundled code, not the whole directory.
.web/build/client/Production HTML, hashed JS/CSS, static assetsBrowser-facing files generated by this build. Browser/proxy caches may retain them according to serving headers.
Python backend memoryImported modules, function/code objects, handler registry, app/ASGI objects, state-manager objectsLong-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 storageMemory objects, serialized disk state, or Redis data, depending on configurationPer-client state retention. This is separate from generated JS. Server-side state serialization is distinct from the public JSON wire representation.
Browser memory / storageJavaScript modules, React contexts, DOM; session token in sessionStorageUI 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>
  1. Import configuration and app. Python evaluates the state class and registers event handlers, variable types, defaults, and computed-variable dependencies. app.add_page records routes and on-load handlers.
  2. Evaluate the page functions. index() returns a tree of components. A reference such as LabState.count is a symbolic Var expression at this point, not a particular visitor's integer.
  3. 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.
  4. 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.
  5. 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.

04 / An empty tab becomes a working app

First load has two different “hydrations”

Figure 01 · Follow the data

From an empty tab to live state

HTTP brings the interface. The socket brings this session’s data. Follow both trips.

RequestResponseLocal work
From an empty tab to live state
01 · Request →

Request the document

The browser requests / from the frontend. This is HTTP, before the Reflex state socket exists.

Timeline & playback controls
1 / 8
Packet inspector

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.

React hydration: attach the client React runtime to the initial document.
Reflex state hydration: synchronize the browser's public state with this client's Python state. These are separate steps with similar names.
Step
Browser / frontend
Python / backend
1
GET / over HTTP. Receive HTML containing the document shell, metadata, styles, and script references.
No user event handler runs just because the frontend serves HTML.
2
Parse HTML → DOM. Load CSS and JS. Run entry.client.js: hydrateRoot(document, HydratedRouter).
The Python server is separately ready to process events.
3
State providers start from generated initialState. A compiled greeting can initially say “Ada”.
The real session may already contain a different name and count.
4
Read/create a token in sessionStorage. Connect to /_event with the token in the connection query.
Socket.IO connects; map the socket SID to a client token.
5
Browser → State.hydrate, then on_load_internal. Tracked browser storage values, if set, add an update_vars_internal event between them. This app has none.
6
Receive full public snapshot. Replace matching fields in each substate context.
Get/create the session state; hydrate marks is_hydrated false and emits state.dict().
7
Receive page="Home", visits=1, then is_hydrated=true.
on_load_internal selects enter_home, chains it, then set_is_hydrated(True).
8
React commits the updated visible UI. The connection stays open for later events.
State manager retains the session state for the next event.

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>
Inside the browser

What happens between a click and the socket?

Figure 06 · Follow the data

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 work
Inside the browser: from DOM click to socket
01 · Local processing

React 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
1 / 10
Packet inspector

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.

MechanismWhat it schedules or storesWhere it lives
Browser event loopJavaScript callbacks, tasks/microtasks, and opportunities to update renderingBrowser runtime
React event handlingDispatches a browser interaction to the component’s registered callbackLoaded React code in the browser
Reflex event_queueReflex event objects awaiting frontend dispatchA module-level JavaScript array in the current document
Socket.IO transportConnection management, encoding and delivery of named messagesBrowser client and backend socket implementation
Python event processorRegistered backend handler work for the sessionPython 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.

ActionEffect
preventDefaultSuppresses the browser’s default action for the event, where applicable. This is distinct from stopping propagation.
stopPropagationStops the interaction propagating to further event handlers through the event system.
ThrottleLimits invocation frequency using a keyed frontend throttle helper.
DebounceDefers the target through a keyed debounce helper; subsequent activity can replace the pending invocation.
TemporalCan 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

  1. 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.
  2. Break in the generated button callback, then addEvents, queueEvents, processEvent, and just before socket.emit. Inspect _e, the separate Reflex event, and the module’s event_queue in scope.
  3. 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.
  4. Break at applyDelta to 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();
};

05 / Step through the round trip

Follow one click, one layer at a time

Figure 02 · Follow the data

A click is a round trip, not new HTML

Watch count stay at 0 in the browser until the server’s delta arrives.

RequestResponseLocal work
A click is a round trip, not new HTML
01 · Local processing

Click 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
1 / 7
Packet inspector

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.

Step 1 of 6 · Simulation

The screen is ready

Both sides agree after hydration. The browser holds public fields; Python also holds _handled.

1 · BrowserDOM + React state
⇄
2 · WireSocket.IO event / delta
⇄
3 · PythonSession state + handler

Browser's app state

Python's app state

Selected layer's data

 
06 / The same information, in different forms

What exactly crosses the boundary?

WhereOriginal formWhat it means
Python declarationcount: int = 0Typed state field and default. A class reference becomes a Var expression when used in a component.
Python session instanceself.count == 1Actual Python integer for this token.
Browser DOM eventevent.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 frame42/_event,["event",{…}]Engine.IO message type 4 + Socket.IO EVENT type 2 + namespace + JSON array.
Python deserializationEvent(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 callrename(value="Lin")The event argument binds to the annotated parameter; self is the resolved session state.
Python update objectStateUpdate(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.

07 / Evidence from the working app

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.

08 / Inside the kitchen

From event name to Python mutation

  1. Connection context: EventNamespace.on_event finds the token associated with the SID. An event from an unassociated socket is not processed as a normal session event.
  2. 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.
  3. 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.
  4. 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.
  5. Middleware and handler: update route variables, allow preprocessing, resolve the event target and arguments, and call the handler. self.count += 1 updates the actual session integer; _handled += 1 updates backend-only bookkeeping.
  6. Dirty tracking: state assignment marks fields dirty. Computed dependency tracking invalidates doubled when count changes. get_delta() collects dirty public base variables and computed variables, adds the field marker, and recurses through dirty substates.
  7. 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.

09 / Back in the browser

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.

10 / A new URL, the same session

What happens when you click Details?

Figure 03 · Follow the data

New page. Same counter. Same connection.

This sequence begins after one click: count is 1 and the Home page has one visit.

RequestResponseLocal work
New page. Same counter. Same connection.
01 · Local processing

Follow the internal link

The app link requests client-side navigation to /details. This is a local browser action.

Timeline & playback controls
1 / 6
Packet inspector

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.

  1. 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.
  2. The event-loop navigation effect marks the local root state as not hydrated and queues on-load processing with the new router data.
  3. The server normalizes the route and updates router state. It selects the registered enter_details handler, followed by setting is_hydrated true.
  4. 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.
ActionDocument / connectionState / on-load
Home → Details linkClient navigation; normally same document and socket.No ordinary full hydrate. on_load_internal runs; same LabState.
Refresh /detailsNew 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 /detailsFrontend host must serve the route/SPA fallback, then load route JS.Hydrate first; Details on-load then runs. A new token gets defaults.
Back / forwardNormally 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 changeSame route can receive different search data.The navigation effect checks pathname + search. Query data accompanies the event. Example: /details?tab=activity.
Hash-only anchorOften 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 linkLeaves 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.

11 / Where does the data live?

Browser copy, session state, and durable records are different

Figure 04 · Follow the data

Why initialState contains more than your counter

Follow build-time placeholders as they become session-specific values. Only public fields cross to React.

RequestResponseLocal work
Why initialState contains more than your counter
01 · ← Response

Ship 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
1 / 5
Packet inspector

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.

12 / Beyond this tiny app

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.

FeatureWhere it fitsWhat changes in the flow
Yield / returned eventsServer event processingA 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 tasksServer schedulerLong-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 actionsBrowser event dispatcherRedirects, scripts, focus, and similar frontend commands can run locally. Not every Reflex event must invoke Python. A server handler can request these commands too.
FormsDOM → event payloadSubmit 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.
UploadsHTTP endpoint + event processingFiles 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 handlingAround event execution and updatesCan 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 integrationPython business logicQuery/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 / styleCompile + React runtimePython 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 JSBoundary adaptersTranslate 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 pagesCompiler + frontendA 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 / metadataBuild and document generationRoute metadata and prerender options affect initial HTML and crawlers. They do not make a build-time snapshot personalized to a future visitor.
Deployment / proxies / scalingServing and state infrastructureConfigure 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 / offlineBrowser event loop + socketThe 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.
13 / Verify with your own eyes

Turn the browser into your packet viewer

  1. Run the app, open the browser developer tools, and select Network. Enable Preserve log, then refresh.
  2. 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.
  3. Inspect JavaScript/CSS requests. In development, modules and the Vite runtime appear separately; production bundles look different.
  4. Filter to WebSocket. Select the backend /_event connection, 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.
  5. 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.
  6. 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.
  7. Click Details. Watch pathname/asPath, router deltas, visits, and is_hydrated. Confirm the count remains.
  8. 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.
Useful breakpoints: 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.
14 / Follow the actual implementation

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.

← All articles

Built to read slowly, inspect directly, and compare against real code. Start with the sample app; then follow one request all the way around.