diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml
new file mode 100644
index 000000000..c7bd1003a
--- /dev/null
+++ b/.github/workflows/performance.yml
@@ -0,0 +1,34 @@
+name: performance-tests
+
+on:
+ push:
+ branches:
+ - "main" # or "master"
+ paths-ignore:
+ - "**/*.md"
+ pull_request:
+ workflow_dispatch:
+
+env:
+ TELEMETRY_ENABLED: false
+ NODE_OPTIONS: "--max_old_space_size=8192"
+ PR_TITLE: ${{ github.event.pull_request.title }}
+ APP_HARNESS_HEADLESS: 1
+ PYTHONUNBUFFERED: 1
+
+jobs:
+ benchmarks:
+ name: Run benchmarks
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/actions/setup_build_env
+ with:
+ python-version: 3.12.8
+ run-poetry-install: true
+ create-venv-at-path: .venv
+ - name: Run benchmarks
+ uses: CodSpeedHQ/action@v3
+ with:
+ token: ${{ secrets.CODSPEED_TOKEN }}
+ run: poetry run pytest benchmarks/test_evaluate.py --codspeed
diff --git a/.gitignore b/.gitignore
index 8bd92964c..29a868796 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,4 @@ requirements.txt
.pyi_generator_last_run
.pyi_generator_diff
reflex.db
+.codspeed
\ No newline at end of file
diff --git a/benchmarks/test_evaluate.py b/benchmarks/test_evaluate.py
new file mode 100644
index 000000000..aa4c8237e
--- /dev/null
+++ b/benchmarks/test_evaluate.py
@@ -0,0 +1,231 @@
+from dataclasses import dataclass
+from typing import cast
+
+import pytest
+
+import reflex as rx
+
+
+class SideBarState(rx.State):
+ """State for the side bar."""
+
+ current_page: rx.Field[str] = rx.field("/")
+
+
+@dataclass(frozen=True)
+class SideBarPage:
+ """A page in the side bar."""
+
+ title: str
+ href: str
+
+
+@dataclass(frozen=True)
+class SideBarSection:
+ """A section in the side bar."""
+
+ name: str
+ icon: str
+ pages: tuple[SideBarPage, ...]
+
+
+@dataclass(frozen=True)
+class Category:
+ """A category in the side bar."""
+
+ name: str
+ href: str
+ sections: tuple[SideBarSection, ...]
+
+
+SIDE_BAR = (
+ Category(
+ name="General",
+ href="/",
+ sections=(
+ SideBarSection(
+ name="Home",
+ icon="home",
+ pages=(
+ SideBarPage(title="Home", href="/"),
+ SideBarPage(title="Contact", href="/contact"),
+ ),
+ ),
+ SideBarSection(
+ name="About",
+ icon="info",
+ pages=(
+ SideBarPage(title="About", href="/about"),
+ SideBarPage(title="FAQ", href="/faq"),
+ ),
+ ),
+ ),
+ ),
+ Category(
+ name="Projects",
+ href="/projects",
+ sections=(
+ SideBarSection(
+ name="Python",
+ icon="worm",
+ pages=(
+ SideBarPage(title="Python", href="/projects/python"),
+ SideBarPage(title="Django", href="/projects/django"),
+ SideBarPage(title="Flask", href="/projects/flask"),
+ SideBarPage(title="FastAPI", href="/projects/fastapi"),
+ SideBarPage(title="Pyramid", href="/projects/pyramid"),
+ SideBarPage(title="Tornado", href="/projects/tornado"),
+ SideBarPage(title="TurboGears", href="/projects/turbogears"),
+ SideBarPage(title="Web2py", href="/projects/web2py"),
+ SideBarPage(title="Zope", href="/projects/zope"),
+ SideBarPage(title="Plone", href="/projects/plone"),
+ SideBarPage(title="Quixote", href="/projects/quixote"),
+ SideBarPage(title="Bottle", href="/projects/bottle"),
+ SideBarPage(title="CherryPy", href="/projects/cherrypy"),
+ SideBarPage(title="Falcon", href="/projects/falcon"),
+ SideBarPage(title="Sanic", href="/projects/sanic"),
+ SideBarPage(title="Starlette", href="/projects/starlette"),
+ ),
+ ),
+ SideBarSection(
+ name="JavaScript",
+ icon="banana",
+ pages=(
+ SideBarPage(title="JavaScript", href="/projects/javascript"),
+ SideBarPage(title="Angular", href="/projects/angular"),
+ SideBarPage(title="React", href="/projects/react"),
+ SideBarPage(title="Vue", href="/projects/vue"),
+ SideBarPage(title="Ember", href="/projects/ember"),
+ SideBarPage(title="Backbone", href="/projects/backbone"),
+ SideBarPage(title="Meteor", href="/projects/meteor"),
+ SideBarPage(title="Svelte", href="/projects/svelte"),
+ SideBarPage(title="Preact", href="/projects/preact"),
+ SideBarPage(title="Mithril", href="/projects/mithril"),
+ SideBarPage(title="Aurelia", href="/projects/aurelia"),
+ SideBarPage(title="Polymer", href="/projects/polymer"),
+ SideBarPage(title="Knockout", href="/projects/knockout"),
+ SideBarPage(title="Dojo", href="/projects/dojo"),
+ SideBarPage(title="Riot", href="/projects/riot"),
+ SideBarPage(title="Alpine", href="/projects/alpine"),
+ SideBarPage(title="Stimulus", href="/projects/stimulus"),
+ SideBarPage(title="Marko", href="/projects/marko"),
+ SideBarPage(title="Sapper", href="/projects/sapper"),
+ SideBarPage(title="Nuxt", href="/projects/nuxt"),
+ SideBarPage(title="Next", href="/projects/next"),
+ SideBarPage(title="Gatsby", href="/projects/gatsby"),
+ SideBarPage(title="Gridsome", href="/projects/gridsome"),
+ SideBarPage(title="Nest", href="/projects/nest"),
+ SideBarPage(title="Express", href="/projects/express"),
+ SideBarPage(title="Koa", href="/projects/koa"),
+ SideBarPage(title="Hapi", href="/projects/hapi"),
+ SideBarPage(title="LoopBack", href="/projects/loopback"),
+ SideBarPage(title="Feathers", href="/projects/feathers"),
+ SideBarPage(title="Sails", href="/projects/sails"),
+ SideBarPage(title="Adonis", href="/projects/adonis"),
+ SideBarPage(title="Meteor", href="/projects/meteor"),
+ SideBarPage(title="Derby", href="/projects/derby"),
+ SideBarPage(title="Socket.IO", href="/projects/socketio"),
+ ),
+ ),
+ ),
+ ),
+)
+
+
+def side_bar_page(page: SideBarPage):
+ return rx.box(
+ rx.link(
+ page.title,
+ href=page.href,
+ )
+ )
+
+
+def side_bar_section(section: SideBarSection):
+ return rx.accordion.item(
+ rx.accordion.header(
+ rx.accordion.trigger(
+ rx.hstack(
+ rx.hstack(
+ rx.icon(section.icon),
+ section.name,
+ align="center",
+ ),
+ rx.accordion.icon(),
+ width="100%",
+ justify="between",
+ )
+ )
+ ),
+ rx.accordion.content(
+ rx.vstack(
+ *map(side_bar_page, section.pages),
+ ),
+ border_inline_start="1px solid",
+ padding_inline_start="1em",
+ margin_inline_start="1.5em",
+ ),
+ value=section.name,
+ width="100%",
+ variant="ghost",
+ )
+
+
+def side_bar_category(category: Category):
+ selected_section = cast(
+ rx.Var,
+ rx.match(
+ SideBarState.current_page,
+ *[
+ (
+ section.name,
+ section.name,
+ )
+ for section in category.sections
+ ],
+ None,
+ ),
+ )
+ return rx.vstack(
+ rx.heading(
+ rx.link(
+ category.name,
+ href=category.href,
+ ),
+ size="5",
+ ),
+ rx.accordion.root(
+ *map(side_bar_section, category.sections),
+ default_value=selected_section.to(str),
+ variant="ghost",
+ width="100%",
+ collapsible=True,
+ type="multiple",
+ ),
+ width="100%",
+ )
+
+
+def side_bar():
+ return rx.vstack(
+ *map(side_bar_category, SIDE_BAR),
+ width="fit-content",
+ )
+
+
+LOREM_IPSUM = "Lorem ipsum dolor sit amet, dolor ut dolore pariatur aliqua enim tempor sed. Labore excepteur sed exercitation. Ullamco aliquip lorem sunt enim in incididunt. Magna anim officia sint cillum labore. Ut eu non dolore minim nostrud magna eu, aute ex in incididunt irure eu. Fugiat et magna magna est excepteur eiusmod minim. Quis eiusmod et non pariatur dolor veniam incididunt, eiusmod irure enim sed dolor lorem pariatur do. Occaecat duis irure excepteur dolore. Proident ut laborum pariatur sit sit, nisi nostrud voluptate magna commodo laborum esse velit. Voluptate non minim deserunt adipiscing irure deserunt cupidatat. Laboris veniam commodo incididunt veniam lorem occaecat, fugiat ipsum dolor cupidatat. Ea officia sed eu excepteur culpa adipiscing, tempor consectetur ullamco eu. Anim ex proident nulla sunt culpa, voluptate veniam proident est adipiscing sint elit velit. Laboris adipiscing est culpa cillum magna. Sit veniam nulla nulla, aliqua eiusmod commodo lorem cupidatat commodo occaecat. Fugiat cillum dolor incididunt mollit eiusmod sint. Non lorem dolore labore excepteur minim laborum sed. Irure nisi do lorem nulla sunt commodo, deserunt quis mollit consectetur minim et esse est, proident nostrud officia enim sed reprehenderit. Magna cillum consequat aute reprehenderit duis sunt ullamco. Labore qui mollit voluptate. Duis dolor sint aute amet aliquip officia, est non mollit tempor enim quis fugiat, eu do culpa consectetur magna. Do ullamco aliqua voluptate culpa excepteur reprehenderit reprehenderit. Occaecat nulla sit est magna. Deserunt ea voluptate veniam cillum. Amet cupidatat duis est tempor fugiat ex eu, officia est sunt consectetur labore esse exercitation. Nisi cupidatat irure est nisi. Officia amet eu veniam reprehenderit. In amet incididunt tempor commodo ea labore. Mollit dolor aliquip excepteur, voluptate aute occaecat id officia proident. Ullamco est amet tempor. Proident aliquip proident mollit do aliquip ipsum, culpa quis aute id irure. Velit excepteur cillum cillum ut cupidatat. Occaecat qui elit esse nulla minim. Consequat velit id ad pariatur tempor. Eiusmod deserunt aliqua ex sed quis non. Dolor sint commodo ex in deserunt nostrud excepteur, pariatur ex aliqua anim adipiscing amet proident. Laboris eu laborum magna lorem ipsum fugiat velit."
+
+
+def complicated_page():
+ return rx.hstack(
+ side_bar(),
+ rx.box(
+ rx.heading("Complicated Page", size="1"),
+ rx.text(LOREM_IPSUM),
+ ),
+ )
+
+
+@pytest.mark.benchmark
+def test_component_init():
+ complicated_page()
diff --git a/poetry.lock b/poetry.lock
index f8d4cf949..125b71b55 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -251,7 +251,7 @@ files = [
{file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
{file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
]
-markers = {main = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and sys_platform == \"linux\" and platform_python_implementation != \"PyPy\" and (python_version <= \"3.11\" or python_version >= \"3.12\")", dev = "os_name == \"nt\" and implementation_name != \"pypy\" and (python_version <= \"3.11\" or python_version >= \"3.12\")"}
+markers = {main = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and sys_platform == \"linux\" and platform_python_implementation != \"PyPy\" and (python_version <= \"3.11\" or python_version >= \"3.12\")", dev = "python_version <= \"3.11\" or python_version >= \"3.12\""}
[package.dependencies]
pycparser = "*"
@@ -495,7 +495,6 @@ files = [
{file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"},
{file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"},
{file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"},
- {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385"},
{file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"},
{file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"},
{file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"},
@@ -506,7 +505,6 @@ files = [
{file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"},
{file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"},
{file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"},
- {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba"},
{file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"},
{file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"},
{file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"},
@@ -1101,7 +1099,7 @@ version = "3.0.0"
description = "Python port of markdown-it. Markdown parsing, done right!"
optional = false
python-versions = ">=3.8"
-groups = ["main"]
+groups = ["main", "dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
@@ -1199,7 +1197,7 @@ version = "0.1.2"
description = "Markdown URL utilities"
optional = false
python-versions = ">=3.7"
-groups = ["main"]
+groups = ["main", "dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
@@ -1690,7 +1688,7 @@ files = [
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
]
-markers = {main = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and sys_platform == \"linux\" and platform_python_implementation != \"PyPy\" and (python_version <= \"3.11\" or python_version >= \"3.12\")", dev = "os_name == \"nt\" and implementation_name != \"pypy\" and (python_version <= \"3.11\" or python_version >= \"3.12\")"}
+markers = {main = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and sys_platform == \"linux\" and platform_python_implementation != \"PyPy\" and (python_version <= \"3.11\" or python_version >= \"3.12\")", dev = "python_version <= \"3.11\" or python_version >= \"3.12\""}
[[package]]
name = "pydantic"
@@ -1853,7 +1851,7 @@ version = "2.19.1"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.8"
-groups = ["main"]
+groups = ["main", "dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"},
@@ -1998,6 +1996,39 @@ aspect = ["aspectlib"]
elasticsearch = ["elasticsearch"]
histogram = ["pygal", "pygaljs", "setuptools"]
+[[package]]
+name = "pytest-codspeed"
+version = "3.1.2"
+description = "Pytest plugin to create CodSpeed benchmarks"
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
+files = [
+ {file = "pytest_codspeed-3.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aed496f873670ce0ea8f980a7c1a2c6a08f415e0ebdf207bf651b2d922103374"},
+ {file = "pytest_codspeed-3.1.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee45b0b763f6b5fa5d74c7b91d694a9615561c428b320383660672f4471756e3"},
+ {file = "pytest_codspeed-3.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c84e591a7a0f67d45e2dc9fd05b276971a3aabcab7478fe43363ebefec1358f4"},
+ {file = "pytest_codspeed-3.1.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6ae6d094247156407770e6b517af70b98862dd59a3c31034aede11d5f71c32c"},
+ {file = "pytest_codspeed-3.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0f264991de5b5cdc118b96fc671386cca3f0f34e411482939bf2459dc599097"},
+ {file = "pytest_codspeed-3.1.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0695a4bcd5ff04e8379124dba5d9795ea5e0cadf38be7a0406432fc1467b555"},
+ {file = "pytest_codspeed-3.1.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc356c8dcaaa883af83310f397ac06c96fac9b8a1146e303d4b374b2cb46a18"},
+ {file = "pytest_codspeed-3.1.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc8a5d0366322a75cf562f7d8d672d28c1cf6948695c4dddca50331e08f6b3d5"},
+ {file = "pytest_codspeed-3.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c5fe7a19b72f54f217480b3b527102579547b1de9fe3acd9e66cb4629ff46c8"},
+ {file = "pytest_codspeed-3.1.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b67205755a665593f6521a98317d02a9d07d6fdc593f6634de2c94dea47a3055"},
+ {file = "pytest_codspeed-3.1.2-py3-none-any.whl", hash = "sha256:5e7ed0315e33496c5c07dba262b50303b8d0bc4c3d10bf1d422a41e70783f1cb"},
+ {file = "pytest_codspeed-3.1.2.tar.gz", hash = "sha256:09c1733af3aab35e94a621aa510f2d2114f65591e6f644c42ca3f67547edad4b"},
+]
+
+[package.dependencies]
+cffi = ">=1.17.1"
+pytest = ">=3.8"
+rich = ">=13.8.1"
+
+[package.extras]
+compat = ["pytest-benchmark (>=5.0.0,<5.1.0)", "pytest-xdist (>=3.6.1,<3.7.0)"]
+lint = ["mypy (>=1.11.2,<1.12.0)", "ruff (>=0.6.5,<0.7.0)"]
+test = ["pytest (>=7.0,<8.0)", "pytest-cov (>=4.0.0,<4.1.0)"]
+
[[package]]
name = "pytest-cov"
version = "6.0.0"
@@ -2362,7 +2393,7 @@ version = "13.9.4"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
optional = false
python-versions = ">=3.8.0"
-groups = ["main"]
+groups = ["main", "dev"]
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
files = [
{file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"},
@@ -3152,4 +3183,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.10, <4.0"
-content-hash = "35c503a68e87896b4f7d7c209dd3fe6d707ebcc1702377cab0a1339554c6ad77"
+content-hash = "822150bcbf41e5cbb61da0a059b41d8971e3c6c974c8af4be7ef55126648aea1"
diff --git a/pyproject.toml b/pyproject.toml
index 6eeb17489..8d0b37a23 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -71,6 +71,7 @@ selenium = ">=4.11.0,<5.0"
pytest-benchmark = ">=4.0.0,<6.0"
playwright = ">=1.46.0"
pytest-playwright = ">=0.5.1"
+pytest-codspeed = "^3.1.2"
[tool.poetry.scripts]
reflex = "reflex.reflex:cli"
diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js
index 61f0d59a2..069e651f9 100644
--- a/reflex/.templates/web/utils/state.js
+++ b/reflex/.templates/web/utils/state.js
@@ -106,6 +106,18 @@ export const getBackendURL = (url_str) => {
return endpoint;
};
+/**
+ * Check if the backend is disabled.
+ *
+ * @returns True if the backend is disabled, false otherwise.
+ */
+export const isBackendDisabled = () => {
+ const cookie = document.cookie
+ .split("; ")
+ .find((row) => row.startsWith("backend-enabled="));
+ return cookie !== undefined && cookie.split("=")[1] == "false";
+};
+
/**
* Determine if any event in the event queue is stateful.
*
@@ -808,7 +820,7 @@ export const useEventLoop = (
// Handle socket connect/disconnect.
useEffect(() => {
// only use websockets if state is present
- if (Object.keys(initialState).length > 1) {
+ if (Object.keys(initialState).length > 1 && !isBackendDisabled()) {
// Initialize the websocket connection.
if (!socket.current) {
connect(
diff --git a/reflex/app.py b/reflex/app.py
index 3ba1ff53f..250addfc9 100644
--- a/reflex/app.py
+++ b/reflex/app.py
@@ -60,7 +60,11 @@ from reflex.components.component import (
ComponentStyle,
evaluate_style_namespaces,
)
-from reflex.components.core.banner import connection_pulser, connection_toaster
+from reflex.components.core.banner import (
+ backend_disabled,
+ connection_pulser,
+ connection_toaster,
+)
from reflex.components.core.breakpoints import set_breakpoints
from reflex.components.core.client_side_routing import (
Default404Page,
@@ -159,9 +163,12 @@ def default_overlay_component() -> Component:
Returns:
The default overlay_component, which is a connection_modal.
"""
+ config = get_config()
+
return Fragment.create(
connection_pulser(),
connection_toaster(),
+ *([backend_disabled()] if config.is_reflex_cloud else []),
*codespaces.codespaces_auto_redirect(),
)
diff --git a/reflex/components/base/error_boundary.py b/reflex/components/base/error_boundary.py
index f328773c2..74867a757 100644
--- a/reflex/components/base/error_boundary.py
+++ b/reflex/components/base/error_boundary.py
@@ -11,10 +11,11 @@ from reflex.event import EventHandler, set_clipboard
from reflex.state import FrontendEventExceptionState
from reflex.vars.base import Var
from reflex.vars.function import ArgsFunctionOperation
+from reflex.vars.object import ObjectVar
def on_error_spec(
- error: Var[Dict[str, str]], info: Var[Dict[str, str]]
+ error: ObjectVar[Dict[str, str]], info: ObjectVar[Dict[str, str]]
) -> Tuple[Var[str], Var[str]]:
"""The spec for the on_error event handler.
diff --git a/reflex/components/base/error_boundary.pyi b/reflex/components/base/error_boundary.pyi
index 2e01c7da0..8d27af0f3 100644
--- a/reflex/components/base/error_boundary.pyi
+++ b/reflex/components/base/error_boundary.pyi
@@ -9,9 +9,10 @@ from reflex.components.component import Component
from reflex.event import BASE_STATE, EventType
from reflex.style import Style
from reflex.vars.base import Var
+from reflex.vars.object import ObjectVar
def on_error_spec(
- error: Var[Dict[str, str]], info: Var[Dict[str, str]]
+ error: ObjectVar[Dict[str, str]], info: ObjectVar[Dict[str, str]]
) -> Tuple[Var[str], Var[str]]: ...
class ErrorBoundary(Component):
diff --git a/reflex/components/component.py b/reflex/components/component.py
index 810104d47..440a408df 100644
--- a/reflex/components/component.py
+++ b/reflex/components/component.py
@@ -1944,7 +1944,7 @@ class StatefulComponent(BaseComponent):
if not should_memoize:
# Determine if any Vars have associated data.
- for prop_var in component._get_vars():
+ for prop_var in component._get_vars(include_children=True):
if prop_var._get_all_var_data():
should_memoize = True
break
@@ -2327,8 +2327,8 @@ class MemoizationLeaf(Component):
"""
comp = super().create(*children, **props)
if comp._get_all_hooks():
- comp._memoization_mode = cls._memoization_mode.copy(
- update={"disposition": MemoizationDisposition.ALWAYS}
+ comp._memoization_mode = dataclasses.replace(
+ comp._memoization_mode, disposition=MemoizationDisposition.ALWAYS
)
return comp
@@ -2457,6 +2457,7 @@ def render_dict_to_var(tag: dict | Component | str, imported_names: set[str]) ->
@dataclasses.dataclass(
eq=False,
frozen=True,
+ slots=True,
)
class LiteralComponentVar(CachedVarOperation, LiteralVar, ComponentVar):
"""A Var that represents a Component."""
diff --git a/reflex/components/core/banner.py b/reflex/components/core/banner.py
index 6479bf3b2..882975f2f 100644
--- a/reflex/components/core/banner.py
+++ b/reflex/components/core/banner.py
@@ -4,8 +4,10 @@ from __future__ import annotations
from typing import Optional
+from reflex import constants
from reflex.components.component import Component
from reflex.components.core.cond import cond
+from reflex.components.datadisplay.logo import svg_logo
from reflex.components.el.elements.typography import Div
from reflex.components.lucide.icon import Icon
from reflex.components.radix.themes.components.dialog import (
@@ -293,7 +295,84 @@ class ConnectionPulser(Div):
)
+class BackendDisabled(Div):
+ """A component that displays a message when the backend is disabled."""
+
+ @classmethod
+ def create(cls, **props) -> Component:
+ """Create a backend disabled component.
+
+ Args:
+ **props: The properties of the component.
+
+ Returns:
+ The backend disabled component.
+ """
+ import reflex as rx
+
+ is_backend_disabled = Var(
+ "backendDisabled",
+ _var_type=bool,
+ _var_data=VarData(
+ hooks={
+ "const [backendDisabled, setBackendDisabled] = useState(false);": None,
+ "useEffect(() => { setBackendDisabled(isBackendDisabled()); }, []);": None,
+ },
+ imports={
+ f"$/{constants.Dirs.STATE_PATH}": [
+ ImportVar(tag="isBackendDisabled")
+ ],
+ },
+ ),
+ )
+
+ return super().create(
+ rx.cond(
+ is_backend_disabled,
+ rx.box(
+ rx.box(
+ rx.card(
+ rx.vstack(
+ svg_logo(),
+ rx.text(
+ "You ran out of compute credits.",
+ ),
+ rx.callout(
+ rx.fragment(
+ "Please upgrade your plan or raise your compute credits at ",
+ rx.link(
+ "Reflex Cloud.",
+ href="https://cloud.reflex.dev/",
+ ),
+ ),
+ width="100%",
+ icon="info",
+ variant="surface",
+ ),
+ ),
+ font_size="20px",
+ font_family='"Inter", "Helvetica", "Arial", sans-serif',
+ variant="classic",
+ ),
+ position="fixed",
+ top="50%",
+ left="50%",
+ transform="translate(-50%, -50%)",
+ width="40ch",
+ max_width="90vw",
+ ),
+ position="fixed",
+ z_index=9999,
+ backdrop_filter="grayscale(1) blur(5px)",
+ width="100dvw",
+ height="100dvh",
+ ),
+ )
+ )
+
+
connection_banner = ConnectionBanner.create
connection_modal = ConnectionModal.create
connection_toaster = ConnectionToaster.create
connection_pulser = ConnectionPulser.create
+backend_disabled = BackendDisabled.create
diff --git a/reflex/components/core/banner.pyi b/reflex/components/core/banner.pyi
index f44ee7992..2ea514965 100644
--- a/reflex/components/core/banner.pyi
+++ b/reflex/components/core/banner.pyi
@@ -350,7 +350,93 @@ class ConnectionPulser(Div):
"""
...
+class BackendDisabled(Div):
+ @overload
+ @classmethod
+ def create( # type: ignore
+ cls,
+ *children,
+ access_key: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+ auto_capitalize: Optional[
+ Union[Var[Union[bool, int, str]], bool, int, str]
+ ] = None,
+ content_editable: Optional[
+ Union[Var[Union[bool, int, str]], bool, int, str]
+ ] = None,
+ context_menu: Optional[
+ Union[Var[Union[bool, int, str]], bool, int, str]
+ ] = None,
+ dir: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+ draggable: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+ enter_key_hint: Optional[
+ Union[Var[Union[bool, int, str]], bool, int, str]
+ ] = None,
+ hidden: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+ input_mode: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+ item_prop: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+ lang: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+ role: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+ slot: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+ spell_check: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+ tab_index: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+ title: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+ style: Optional[Style] = None,
+ key: Optional[Any] = None,
+ id: Optional[Any] = None,
+ class_name: Optional[Any] = None,
+ autofocus: Optional[bool] = None,
+ custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None,
+ on_blur: Optional[EventType[[], BASE_STATE]] = None,
+ on_click: Optional[EventType[[], BASE_STATE]] = None,
+ on_context_menu: Optional[EventType[[], BASE_STATE]] = None,
+ on_double_click: Optional[EventType[[], BASE_STATE]] = None,
+ on_focus: Optional[EventType[[], BASE_STATE]] = None,
+ on_mount: Optional[EventType[[], BASE_STATE]] = None,
+ on_mouse_down: Optional[EventType[[], BASE_STATE]] = None,
+ on_mouse_enter: Optional[EventType[[], BASE_STATE]] = None,
+ on_mouse_leave: Optional[EventType[[], BASE_STATE]] = None,
+ on_mouse_move: Optional[EventType[[], BASE_STATE]] = None,
+ on_mouse_out: Optional[EventType[[], BASE_STATE]] = None,
+ on_mouse_over: Optional[EventType[[], BASE_STATE]] = None,
+ on_mouse_up: Optional[EventType[[], BASE_STATE]] = None,
+ on_scroll: Optional[EventType[[], BASE_STATE]] = None,
+ on_unmount: Optional[EventType[[], BASE_STATE]] = None,
+ **props,
+ ) -> "BackendDisabled":
+ """Create a backend disabled component.
+
+ Args:
+ access_key: Provides a hint for generating a keyboard shortcut for the current element.
+ auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
+ content_editable: Indicates whether the element's content is editable.
+ context_menu: Defines the ID of a