Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,26 @@ jobs:
- name: Generate coverage report
run: uv run coverage html

js-unit-tests:
# Unit tests for the javascript Reflex ships in
# `reflex-base/.templates/web`, which the python suites can only reach
# through compiled output.
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
cache: npm
cache-dependency-path: tests/js/package-lock.json
- run: npm ci
working-directory: tests/js
- run: npm test
working-directory: tests/js

unit-tests-macos:
timeout-minutes: 30
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ reflex.db
.env.*
node_modules
package-lock.json
# ...except the javascript test harness, which CI installs with `npm ci`.
!tests/js/package-lock.json
*.pyi
.pre-commit-config.yaml
.claude/.worktrees
Expand All @@ -33,3 +35,4 @@ CLAUDE.local.md

# Backups written by scripts/delete_automated_releases.sh
automated-releases-backup-*.json
tests/js/node_modules
20 changes: 20 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ uv run python scripts/check_min_deps.py # validate each
uv run python scripts/check_min_deps.py --check-dev-pins [pkg] # publish gate: fail if pkg (default: all) declares an unpublishable *.dev dependency pin
uv run python scripts/make_pyi.py # regenerate .pyi stubs
uv run pre-commit run --all-files # all pre-commit hooks
npm --prefix tests/js ci && npm --prefix tests/js test # javascript unit tests (frontend templates)
```

## Layout
Expand All @@ -31,6 +32,7 @@ uv run pre-commit run --all-files # all pre-commi
reflex/ # main framework package (app, state, compiler, components, utils, istate)
packages/ # workspace sub-packages (reflex-base, reflex-components-*, reflex-docgen, reflex-components-internal)
tests/units/ # unit tests, mirrors source tree
tests/js/ # vitest unit tests for the shipped frontend javascript
tests/integration/ # Selenium integration tests (run in dev+prod modes)
tests_playwright/ # Playwright integration tests (preferred for new tests)
tests/benchmarks/ # performance benchmarks
Expand Down Expand Up @@ -58,6 +60,24 @@ docs/ # documentation site (separate workspace member)
- unit tests should primarily cover a single module, and should be named accordingly, including subdirectories (e.g. `tests/units/istate/test_manager.py` for `reflex/istate/manager.py`). For subpackages, also include the corresponding path below `src/` (e.g. `tests/units/reflex_base/event/test_context.py` for `packages/reflex-base/src/reflex_base/event/context.py`).
- **Integration tests:** prefer Playwright (`tests/integration/tests_playwright/`). Integration tests are slow — extend existing test apps rather than creating new ones for trivial functionality. Multiple test cases sharing one app is fine.

### Frontend javascript tests

The javascript Reflex ships lives in
`packages/reflex-base/src/reflex_base/.templates/web/` and is copied verbatim into a
user's `.web` directory, so tests must **not** live inside that tree. They go in
`tests/js/`, which has its own `package.json` and runs under vitest + jsdom:
`npm --prefix tests/js test`.

Reach for these when behavior can only be observed at runtime and an integration
test would be indirect or impossible to set up — provider teardown, SSR vs. client
branches, subscription bookkeeping. Prefer a Playwright test when the thing you
want to assert is visible in a real app.

`$/...` specifiers resolve to the template tree via a vitest alias. `$/utils/state`
is stubbed (`tests/js/stubs/state.js`) because the real module pulls in socket.io,
react-router and the per-app *generated* `utils/context.js`; a module needing those
is not currently unit-testable.

### Integration test patterns

Apps as factory functions, run via `AppHarness`:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import reflex as rx
import reflex_components_internal as ui
from reflex.experimental import ClientStateVar
from reflex_site_shared.integrations import get_integration_logo_url

from .integration_list import get_integration_path
from .integration_request import request_integration_dialog

selected_filter = ClientStateVar.create("selected_filter", "All")
selected_filter = rx.client_state("All", name="selected_filter")

FilterOptions = [
{"name": "AI", "icon": "BotIcon"},
Expand All @@ -29,7 +28,7 @@ def integration_filter_button(data: dict):
variant="outline",
class_name="flex flex-row items-center "
+ rx.cond(selected_filter.value == data["name"], active_pill, "").to(str),
on_click=selected_filter.set_value(data["name"]),
on_click=selected_filter.set(data["name"]),
)


Expand Down
7 changes: 3 additions & 4 deletions docs/app/reflex_docs/templates/docpage/docpage.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import reflex as rx
import reflex_components_internal as ui
from reflex.components.radix.themes.base import LiteralAccentColor
from reflex.experimental.client_state import ClientStateVar
from reflex.utils.format import to_snake_case, to_title_case
from reflex_site_shared.components.blocks.code import *
from reflex_site_shared.components.blocks.demo import *
Expand Down Expand Up @@ -86,7 +85,7 @@ def feedback_button_toc() -> rx.Component:

@rx.memo
def copy_to_markdown(text: rx.Var[str]) -> rx.Component:
copied = ClientStateVar.create("is_copied", default=False, global_ref=False)
copied = rx.client_state(False)
return marketing_button(
rx.cond(
copied.value,
Expand All @@ -101,10 +100,10 @@ def copy_to_markdown(text: rx.Var[str]) -> rx.Component:
variant="ghost",
class_name="justify-start pl-0 text-secondary-11",
on_click=[
rx.call_function(copied.set_value(True)),
rx.call_function(copied.set(True)),
rx.set_clipboard(text),
],
on_mouse_down=rx.call_function(copied.set_value(False)).debounce(1500),
on_mouse_down=rx.call_function(copied.set(False)).debounce(1500),
)


Expand Down
5 changes: 2 additions & 3 deletions docs/library/data-display/icon.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ components:
import reflex as rx

from reflex_components_lucide.icon import LUCIDE_ICON_LIST
from reflex.experimental.client_state import ClientStateVar

icon_search_cs = ClientStateVar.create("icon_search", default="")
icon_search_cs = rx.client_state("", name="icon_search")


@rx.memo
Expand All @@ -26,7 +25,7 @@ def lucide_icons() -> rx.Component:
),
rx.el.input(
placeholder="Search icons...",
on_change=icon_search_cs.set_value,
on_change=icon_search_cs.set,
class_name="relative box-border border-secondary-4 focus:border-violet-9 focus:border-1 bg-secondary-2 p-[0.5rem_0.75rem] border rounded-xl font-base text-secondary-11 placeholder:text-secondary-9 outline-none focus:outline-none w-full mb-2 pl-10",
),
class_name="relative flex items-center",
Expand Down
76 changes: 76 additions & 0 deletions docs/library/dynamic-rendering/foreach.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,84 @@ def foreach_complex_dict_example():
)
```

## Per-Item State And Event Handlers

Each rendered item gets its own scope, so the item and index are available
anywhere in that item's subtree -- including in event handlers and in
components the compiler splits out on its own:

```python
class TodoState(rx.State):
items: list[str] = ["write docs", "ship it"]

@rx.event
def done(self, item: str, index: int): ...


def todo_row(item: rx.Var[str], index: rx.Var[int]) -> rx.Component:
return rx.hstack(
rx.text(item),
rx.button("done", on_click=TodoState.done(item, index)),
)


def todo_list():
return rx.vstack(rx.foreach(TodoState.items, todo_row))
```

Client state works the same way: an unnamed `rx.client_state` var in a
`foreach` body is per item, the way `useState` would be in a React list.

```python
def expandable_row(item: rx.Var[str]) -> rx.Component:
expanded = rx.client_state(False) # one per rendered row
return rx.vstack(
rx.button(item, on_click=expanded.set(lambda prev: ~prev)),
rx.cond(expanded.value, rx.text(f"details for {item}")),
)
```

The default can be the loop item or index, which seeds each row from its own
value:

```python
def counter_row(item: rx.Var[str], index: rx.Var[int]) -> rx.Component:
count = rx.client_state(index) # row N starts at N
return rx.hstack(
rx.text(item),
rx.heading(count.value),
rx.button("+", on_click=count.set(lambda prev: prev + 1)),
)
```

A default is a *seed*: it is read once, when the row first claims the slot, so a
later change to the var does not reset a row that has already been edited. To
push a new value in, set it explicitly -- `on_mount=count.set(index)`, or on a
`rx.fragment(key=..., on_mount=...)` when you want the reset keyed to something.

Loops nest, and each level gets its own scope. A nested body can read an
*enclosing* loop's item and index, as long as it does not reuse their names --
the same rule Python already imposes, since an inner argument of the same name
shadows the outer one:

```python
rx.foreach(
State.rows,
lambda row: rx.foreach(row, lambda cell: rx.text(f"{row[0]}/{cell}")),
)
```

By default each item is keyed by its position in the list. Pass `key=` on the
item to key by identity instead, which is what preserves a row's DOM state
(a typed-in value, focus, an in-flight animation) when the list is reordered:

```python
rx.foreach(TodoState.items, lambda item: todo_row(item, key=item))
```

## API Reference


### `rx.foreach`

```python
Expand Down
109 changes: 106 additions & 3 deletions docs/wrapping-react/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ We also have a var `color` which is the current color of the color picker.
Since this component has interaction we must specify any event triggers that the component takes. The color picker has a single trigger `on_change` to specify when the color changes. This trigger takes in a single argument `color` which is the new color.

```python exec
from reflex.experimental.client_state import ClientStateVar
from reflex.components.component import NoSSRComponent


Expand All @@ -71,15 +70,15 @@ class ColorPicker(NoSSRComponent):

color_picker = ColorPicker.create

ColorPickerState = ClientStateVar.create(default="#db114b", var_name="color")
ColorPickerState = rx.client_state("#db114b", name="color")
```

```python eval
rx.box(
ColorPickerState,
rx.vstack(
rx.heading(ColorPickerState.value, as_="h2", color="white"),
color_picker(on_change=ColorPickerState.set_value),
color_picker(on_change=ColorPickerState.set),
),
background_color=ColorPickerState.value,
padding="5em",
Expand Down Expand Up @@ -122,6 +121,110 @@ def index():
)
```

## Naming And Scoping Client State

Whether a client state var is shared app-wide follows from whether you name it:

```python
shared = rx.client_state(
"", name="search_query"
) # global: any component, and the backend
private = rx.client_state("") # scoped to the component tree using it
```

A **named** var resolves in one app-wide store, so any component can read and write it and
`push`, `retrieve`, `global_value` and `global_set` all work against it.

An **unnamed** var gets a compile-time name and is scoped: the first component in a tree to
use it claims it for that tree, so each instance of that component gets its own state, the
way React's `useState` does. Nothing outside the tree can address it.

Where you *construct* the var decides who shares it. Construct it inside a component and
each instance gets its own:

```python
@rx.memo
def copy_button(text: rx.Var[str]) -> rx.Component:
copied = rx.client_state(False) # one per rendered button
...
```

Construct it once at module level and reference it from several components and they share
it, owned by the outermost one that uses it — the same way lifting state up works in React.
Note the consequence: adding a read at page level makes the page the owner, which collapses
per-instance state below it into one shared value.

A plain helper function called several times gives each call its own state for free, since
the var is constructed once per call:

```python
def counter():
count = rx.client_state(0) # a distinct var per call
return rx.hstack(
rx.button("-", on_click=count.set(lambda v: v - 1)),
rx.heading(count.value),
rx.button("+", on_click=count.set(lambda v: v + 1)),
)


rx.vstack(counter(), counter(), counter()) # three independent counters
```

Each item rendered by `rx.foreach` is its own scope too, so an unnamed var used in a
loop body is per item, and can be seeded from the loop item or index:

```python
def row(item: rx.Var[str], index: rx.Var[int]) -> rx.Component:
expanded = rx.client_state(False) # one per rendered row
count = rx.client_state(index) # row N starts at N
...


rx.foreach(State.items, row)
```

A default is read once, when the scope first claims the name, so it seeds the state
rather than tracking the var. Set the value explicitly (`on_mount=count.set(index)`)
when you need it to follow.

Pass `prefix=` to make generated names readable in the compiled output:
`rx.client_state(0, prefix="counter")`.

## Setting Client State From Plain JavaScript

`value` and `set` are the normal way to use a client state var, but they resolve to a
hook, so they only work inside a component that Reflex renders. When you are wrapping a
library that hands you a plain JavaScript callback -- or you are writing your own JS in
`add_custom_code` -- use `global_value` and `global_set` instead. They need no hook, so
they work anywhere in your compiled page:

```python
picker_color = rx.client_state("#db114b", name="picker_color")


class MyPicker(rx.Component):
library = "some-non-react-picker"
tag = "Picker"

def add_custom_code(self) -> list[str]:
# `global_set` is a plain function, so a non-React callback can call it.
return [f"const onPickerChange = {picker_color.global_set};"]
```

Reads through `global_value` are a point-in-time snapshot with no reactivity, so prefer
`value` inside components. Writes through `global_set` re-render every component
subscribed to that var, exactly like `set` does. Both require a **named** client state
var, since the name is what identifies the value.

`rx.call_script` is the one place these do not work: its code is evaluated inside the
Reflex runtime module, so your page's imports are not in scope there. Reach the store
through the `refs` object instead, which is also how you inspect client state from the
browser devtools console:

```python
rx.call_script('refs["__client_state"].set("picker_color", "#ffffff")')
```

## What Not To Wrap

There are some libraries on npm that are not do not expose React components and therefore are very hard to wrap with Reflex.
Expand Down
Loading
Loading