Browse Source

Merge branch 'main' into add-validation-to-function-vars

Khaleel Al-Adhami 4 tháng trước cách đây
mục cha
commit
99a3090784
100 tập tin đã thay đổi với 1449 bổ sung497 xóa
  1. 1 1
      .github/actions/setup_build_env/action.yml
  2. 3 3
      .github/workflows/benchmarks.yml
  3. 1 1
      .github/workflows/check_outdated_dependencies.yml
  4. 2 2
      .github/workflows/integration_app_harness.yml
  5. 5 5
      .github/workflows/integration_tests.yml
  6. 4 3
      .github/workflows/unit_tests.yml
  7. 6 0
      .pre-commit-config.yaml
  8. 1 1
      CODE_OF_CONDUCT.md
  9. 1 1
      README.md
  10. 2 1
      benchmarks/benchmark_compile_times.py
  11. 2 1
      benchmarks/benchmark_imports.py
  12. 2 2
      docker-example/production-app-platform/Dockerfile
  13. 2 2
      docker-example/production-compose/Dockerfile
  14. 1 1
      docker-example/production-compose/compose.prod.yaml
  15. 19 21
      pyproject.toml
  16. 5 1
      reflex/.templates/jinja/web/pages/stateful_component.js.jinja2
  17. 1 1
      reflex/.templates/jinja/web/utils/context.js.jinja2
  18. 35 27
      reflex/.templates/web/utils/state.js
  19. 21 14
      reflex/app.py
  20. 8 7
      reflex/base.py
  21. 1 0
      reflex/compiler/templates.py
  22. 2 3
      reflex/compiler/utils.py
  23. 47 23
      reflex/components/component.py
  24. 1 1
      reflex/components/core/banner.py
  25. 1 1
      reflex/components/core/breakpoints.py
  26. 10 8
      reflex/components/core/clipboard.py
  27. 1 1
      reflex/components/core/clipboard.pyi
  28. 5 3
      reflex/components/core/upload.py
  29. 1 1
      reflex/components/datadisplay/code.py
  30. 6 3
      reflex/components/datadisplay/dataeditor.py
  31. 1 1
      reflex/components/datadisplay/dataeditor.pyi
  32. 1 1
      reflex/components/el/elements/__init__.py
  33. 1 1
      reflex/components/el/elements/__init__.pyi
  34. 28 0
      reflex/components/el/elements/forms.py
  35. 2 2
      reflex/components/el/elements/forms.pyi
  36. 1 1
      reflex/components/el/elements/metadata.py
  37. 3 3
      reflex/components/plotly/plotly.py
  38. 3 3
      reflex/components/plotly/plotly.pyi
  39. 1 1
      reflex/components/radix/primitives/slider.py
  40. 11 0
      reflex/components/radix/themes/components/context_menu.py
  41. 155 0
      reflex/components/radix/themes/components/context_menu.pyi
  42. 1 1
      reflex/components/radix/themes/components/icon_button.py
  43. 16 1
      reflex/components/radix/themes/components/text_field.py
  44. 103 15
      reflex/components/radix/themes/components/text_field.pyi
  45. 1 1
      reflex/components/radix/themes/layout/center.pyi
  46. 1 1
      reflex/components/radix/themes/layout/flex.py
  47. 1 1
      reflex/components/radix/themes/layout/flex.pyi
  48. 1 1
      reflex/components/radix/themes/layout/grid.py
  49. 1 1
      reflex/components/radix/themes/layout/grid.pyi
  50. 1 1
      reflex/components/radix/themes/layout/spacer.pyi
  51. 3 3
      reflex/components/radix/themes/layout/stack.pyi
  52. 2 2
      reflex/components/recharts/cartesian.py
  53. 6 6
      reflex/components/recharts/cartesian.pyi
  54. 4 4
      reflex/components/recharts/charts.py
  55. 2 2
      reflex/components/recharts/polar.py
  56. 2 2
      reflex/components/recharts/polar.pyi
  57. 7 4
      reflex/config.py
  58. 1 1
      reflex/constants/base.py
  59. 6 0
      reflex/constants/compiler.py
  60. 2 0
      reflex/constants/config.py
  61. 1 1
      reflex/constants/custom_components.py
  62. 1 1
      reflex/constants/route.py
  63. 21 21
      reflex/custom_components/custom_components.py
  64. 53 18
      reflex/event.py
  65. 3 3
      reflex/istate/data.py
  66. 11 13
      reflex/model.py
  67. 1 1
      reflex/page.py
  68. 5 5
      reflex/reflex.py
  69. 162 82
      reflex/state.py
  70. 8 8
      reflex/testing.py
  71. 1 1
      reflex/utils/build.py
  72. 60 6
      reflex/utils/console.py
  73. 4 0
      reflex/utils/exceptions.py
  74. 5 5
      reflex/utils/exec.py
  75. 1 2
      reflex/utils/export.py
  76. 6 2
      reflex/utils/format.py
  77. 2 2
      reflex/utils/path_ops.py
  78. 45 29
      reflex/utils/prerequisites.py
  79. 5 5
      reflex/utils/processes.py
  80. 4 5
      reflex/utils/pyi_generator.py
  81. 6 4
      reflex/utils/telemetry.py
  82. 1 0
      reflex/vars/__init__.py
  83. 37 27
      reflex/vars/base.py
  84. 222 0
      reflex/vars/datetime.py
  85. 2 2
      reflex/vars/function.py
  86. 1 1
      reflex/vars/number.py
  87. 1 1
      reflex/vars/sequence.py
  88. 4 5
      scripts/wait_for_listening_port.py
  89. 23 0
      tests/integration/conftest.py
  90. 2 2
      tests/integration/test_call_script.py
  91. 4 5
      tests/integration/test_client_storage.py
  92. 2 0
      tests/integration/test_exception_handlers.py
  93. 15 2
      tests/integration/test_upload.py
  94. 87 0
      tests/integration/tests_playwright/test_datetime_operations.py
  95. 3 3
      tests/units/components/core/test_banner.py
  96. 1 1
      tests/units/components/core/test_cond.py
  97. 18 0
      tests/units/components/core/test_foreach.py
  98. 9 12
      tests/units/states/upload.py
  99. 9 9
      tests/units/test_db_config.py
  100. 38 14
      tests/units/test_health_endpoint.py

+ 1 - 1
.github/actions/setup_build_env/action.yml

@@ -6,7 +6,7 @@
 #
 # Exit conditions:
 # - Python of version `python-version` is ready to be invoked as `python`.
-# - Poetry of version `poetry-version` is ready ot be invoked as `poetry`.
+# - Poetry of version `poetry-version` is ready to be invoked as `poetry`.
 # - If `run-poetry-install` is true, deps as defined in `pyproject.toml` will have been installed into the venv at `create-venv-at-path`.
 
 name: 'Setup Reflex build environment'

+ 3 - 3
.github/workflows/benchmarks.yml

@@ -80,7 +80,7 @@ jobs:
       fail-fast: false
       matrix:
         # Show OS combos first in GUI
-        os: [ubuntu-latest, windows-latest, macos-12]
+        os: [ubuntu-latest, windows-latest, macos-latest]
         python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0']
         exclude:
           - os: windows-latest
@@ -92,7 +92,7 @@ jobs:
             python-version: '3.9.18'
           - os: macos-latest
             python-version: '3.10.13'
-          - os: macos-12
+          - os: macos-latest
             python-version: '3.12.0'
         include:
           - os: windows-latest
@@ -155,7 +155,7 @@ jobs:
       fail-fast: false
       matrix:
         # Show OS combos first in GUI
-        os: [ubuntu-latest, windows-latest, macos-12]
+        os: [ubuntu-latest, windows-latest, macos-latest]
         python-version: ['3.11.5']
 
     runs-on: ${{ matrix.os }}

+ 1 - 1
.github/workflows/check_outdated_dependencies.yml

@@ -58,7 +58,7 @@ jobs:
       working-directory: ./reflex-web
       run: poetry run uv pip install -r requirements.txt
     - name: Install additional dependencies for DB access
-      run: poetry run uv pip install psycopg2-binary
+      run: poetry run uv pip install psycopg
     - name: Init Website for reflex-web
       working-directory: ./reflex-web
       run: poetry run reflex init

+ 2 - 2
.github/workflows/integration_app_harness.yml

@@ -22,9 +22,9 @@ jobs:
     timeout-minutes: 30
     strategy:
       matrix:
-        state_manager: ["redis", "memory"]
+        state_manager: ['redis', 'memory']
+        python-version: ['3.11.5', '3.12.0', '3.13.0']
         split_index: [1, 2]
-        python-version: ["3.11.5", "3.12.0"]
       fail-fast: false
     runs-on: ubuntu-22.04
     services:

+ 5 - 5
.github/workflows/integration_tests.yml

@@ -43,7 +43,7 @@ jobs:
       matrix:
         # Show OS combos first in GUI
         os: [ubuntu-latest, windows-latest]
-        python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0']
+        python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0', '3.13.0']
         exclude:
           - os: windows-latest
             python-version: '3.10.13'
@@ -73,7 +73,7 @@ jobs:
         run: |
           poetry run uv pip install -r requirements.txt
       - name: Install additional dependencies for DB access
-        run: poetry run uv pip install psycopg2-binary
+        run: poetry run uv pip install psycopg
       - name: Check export --backend-only before init for counter example
         working-directory: ./reflex-examples/counter
         run: |
@@ -147,7 +147,7 @@ jobs:
         working-directory: ./reflex-web
         run: poetry run uv pip install $(grep -ivE "reflex " requirements.txt)
       - name: Install additional dependencies for DB access
-        run: poetry run uv pip install psycopg2-binary
+        run: poetry run uv pip install psycopg
       - name: Init Website for reflex-web
         working-directory: ./reflex-web
         run: poetry run reflex init
@@ -198,7 +198,7 @@ jobs:
       fail-fast: false
       matrix:
         python-version: ['3.11.5', '3.12.0']
-    runs-on: macos-12
+    runs-on: macos-latest
     steps:
       - uses: actions/checkout@v4
       - uses: ./.github/actions/setup_build_env
@@ -216,7 +216,7 @@ jobs:
         working-directory: ./reflex-web
         run: poetry run uv pip install -r requirements.txt
       - name: Install additional dependencies for DB access
-        run: poetry run uv pip install psycopg2-binary
+        run: poetry run uv pip install psycopg
       - name: Init Website for reflex-web
         working-directory: ./reflex-web
         run: poetry run reflex init

+ 4 - 3
.github/workflows/unit_tests.yml

@@ -28,7 +28,7 @@ jobs:
       fail-fast: false
       matrix:
         os: [ubuntu-latest, windows-latest]
-        python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0']
+        python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0', '3.13.0']
         # Windows is a bit behind on Python version availability in Github
         exclude:
           - os: windows-latest
@@ -88,8 +88,9 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0']
-    runs-on: macos-12
+        # Note: py39, py310 versions chosen due to available arm64 darwin builds.
+        python-version: ['3.9.13', '3.10.11', '3.11.5', '3.12.0', '3.13.0']
+    runs-on: macos-latest
     steps:
       - uses: actions/checkout@v4
       - uses: ./.github/actions/setup_build_env

+ 6 - 0
.pre-commit-config.yaml

@@ -11,6 +11,12 @@ repos:
         args: ["--fix", "--exit-non-zero-on-fix"]
         exclude: '^integration/benchmarks/'
 
+  - repo: https://github.com/codespell-project/codespell
+    rev: v2.3.0
+    hooks:
+      - id: codespell
+        args: ["reflex"]
+
   # Run pyi check before pyright because pyright can fail if pyi files are wrong.
   - repo: local
     hooks:

+ 1 - 1
CODE_OF_CONDUCT.md

@@ -5,7 +5,7 @@
 We as members, contributors, and leaders pledge to make participation in our
 community a harassment-free experience for everyone, regardless of age, body
 size, visible or invisible disability, ethnicity, sex characteristics, gender
-identity and expression, level of experience, education, socio-economic status,
+identity and expression, level of experience, education, socioeconomic status,
 nationality, personal appearance, race, religion, or sexual identity
 and orientation.
 

+ 1 - 1
README.md

@@ -249,7 +249,7 @@ We welcome contributions of any size! Below are some good ways to get started in
 -   **GitHub Discussions**: A great way to talk about features you want added or things that are confusing/need clarification.
 -   **GitHub Issues**: [Issues](https://github.com/reflex-dev/reflex/issues) are an excellent way to report bugs. Additionally, you can try and solve an existing issue and submit a PR.
 
-We are actively looking for contributors, no matter your skill level or experience. To contribute check out [CONTIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md)
+We are actively looking for contributors, no matter your skill level or experience. To contribute check out [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md)
 
 
 ## All Thanks To Our Contributors:

+ 2 - 1
benchmarks/benchmark_compile_times.py

@@ -5,6 +5,7 @@ from __future__ import annotations
 import argparse
 import json
 import os
+from pathlib import Path
 
 from utils import send_data_to_posthog
 
@@ -18,7 +19,7 @@ def extract_stats_from_json(json_file: str) -> list[dict]:
     Returns:
         list[dict]: The stats for each test.
     """
-    with open(json_file, "r") as file:
+    with Path(json_file).open() as file:
         json_data = json.load(file)
 
     # Load the JSON data if it is a string, otherwise assume it's already a dictionary

+ 2 - 1
benchmarks/benchmark_imports.py

@@ -5,6 +5,7 @@ from __future__ import annotations
 import argparse
 import json
 import os
+from pathlib import Path
 
 from utils import send_data_to_posthog
 
@@ -18,7 +19,7 @@ def extract_stats_from_json(json_file: str) -> dict:
     Returns:
         dict: The stats for each test.
     """
-    with open(json_file, "r") as file:
+    with Path(json_file).open() as file:
         json_data = json.load(file)
 
     # Load the JSON data if it is a string, otherwise assume it's already a dictionary

+ 2 - 2
docker-example/production-app-platform/Dockerfile

@@ -27,7 +27,7 @@ FROM python:3.13 as init
 
 ARG uv=/root/.local/bin/uv
 
-# Install `uv` for faster package boostrapping
+# Install `uv` for faster package bootstrapping
 ADD --chmod=755 https://astral.sh/uv/install.sh /install.sh
 RUN /install.sh && rm /install.sh
 
@@ -52,7 +52,7 @@ FROM python:3.13-slim
 WORKDIR /app
 RUN adduser --disabled-password --home /app reflex
 COPY --chown=reflex --from=init /app /app
-# Install libpq-dev for psycopg2 (skip if not using postgres).
+# Install libpq-dev for psycopg (skip if not using postgres).
 RUN apt-get update -y && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/*
 USER reflex
 ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1

+ 2 - 2
docker-example/production-compose/Dockerfile

@@ -6,7 +6,7 @@ FROM python:3.13 as init
 
 ARG uv=/root/.local/bin/uv
 
-# Install `uv` for faster package boostrapping
+# Install `uv` for faster package bootstrapping
 ADD --chmod=755 https://astral.sh/uv/install.sh /install.sh
 RUN /install.sh && rm /install.sh
 
@@ -39,7 +39,7 @@ FROM python:3.13-slim
 WORKDIR /app
 RUN adduser --disabled-password --home /app reflex
 COPY --chown=reflex --from=init /app /app
-# Install libpq-dev for psycopg2 (skip if not using postgres).
+# Install libpq-dev for psycopg (skip if not using postgres).
 RUN apt-get update -y && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/*
 USER reflex
 ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1

+ 1 - 1
docker-example/production-compose/compose.prod.yaml

@@ -15,7 +15,7 @@ services:
 
   app:
     environment:
-      DB_URL: postgresql+psycopg2://postgres:secret@db/postgres
+      DB_URL: postgresql+psycopg://postgres:secret@db/postgres
       REDIS_URL: redis://redis:6379
     depends_on:
       - db

+ 19 - 21
pyproject.toml

@@ -1,29 +1,22 @@
 [tool.poetry]
 name = "reflex"
-version = "0.6.7dev1"
+version = "0.6.8dev1"
 description = "Web apps in pure Python."
 license = "Apache-2.0"
 authors = [
-    "Nikhil Rao <nikhil@reflex.dev>",
-    "Alek Petuskey <alek@reflex.dev>",
-    "Masen Furer <masen@reflex.dev>",
-    "Elijah Ahianyo <elijah@reflex.dev>",
-    "Thomas Brandého <thomas@reflex.dev>",
+  "Nikhil Rao <nikhil@reflex.dev>",
+  "Alek Petuskey <alek@reflex.dev>",
+  "Masen Furer <masen@reflex.dev>",
+  "Elijah Ahianyo <elijah@reflex.dev>",
+  "Thomas Brandého <thomas@reflex.dev>",
 ]
 readme = "README.md"
 homepage = "https://reflex.dev"
 repository = "https://github.com/reflex-dev/reflex"
 documentation = "https://reflex.dev/docs/getting-started/introduction"
-keywords = [
-    "web",
-    "framework",
-]
-classifiers = [
-    "Development Status :: 4 - Beta",
-]
-packages = [
-    {include = "reflex"}
-]
+keywords = ["web", "framework"]
+classifiers = ["Development Status :: 4 - Beta"]
+packages = [{ include = "reflex" }]
 
 [tool.poetry.dependencies]
 python = "^3.9"
@@ -42,11 +35,11 @@ uvicorn = ">=0.20.0"
 starlette-admin = ">=0.11.0,<1.0"
 alembic = ">=1.11.1,<2.0"
 platformdirs = ">=3.10.0,<5.0"
-distro = {version = ">=1.8.0,<2.0", platform = "linux"}
+distro = { version = ">=1.8.0,<2.0", platform = "linux" }
 python-engineio = "!=4.6.0"
 wrapt = [
-    {version = ">=1.14.0,<2.0", python = ">=3.11"},
-    {version = ">=1.11.0,<2.0", python = "<3.11"},
+  { version = ">=1.14.0,<2.0", python = ">=3.11" },
+  { version = ">=1.11.0,<2.0", python = "<3.11" },
 ]
 packaging = ">=23.1,<25.0"
 reflex-hosting-cli = ">=0.1.29,<2.0"
@@ -93,14 +86,15 @@ reportIncompatibleMethodOverride = false
 
 [tool.ruff]
 target-version = "py39"
+output-format = "concise"
 lint.isort.split-on-trailing-comma = false
-lint.select = ["B", "D", "E", "F", "I", "SIM", "W", "RUF", "FURB", "ERA"]
+lint.select = ["B", "C4", "D", "E", "ERA", "F", "FURB", "I", "PERF", "PTH", "RUF", "SIM", "W"]
 lint.ignore = ["B008", "D205", "E501", "F403", "SIM115", "RUF006", "RUF012"]
 lint.pydocstyle.convention = "google"
 
 [tool.ruff.lint.per-file-ignores]
 "__init__.py" = ["F401"]
-"tests/*.py" = ["D100", "D103", "D104", "B018"]
+"tests/*.py" = ["D100", "D103", "D104", "B018", "PERF"]
 "reflex/.templates/*.py" = ["D100", "D103", "D104"]
 "*.pyi" = ["D301", "D415", "D417", "D418", "E742"]
 "*/blank.py" = ["I001"]
@@ -108,3 +102,7 @@ lint.pydocstyle.convention = "google"
 [tool.pytest.ini_options]
 asyncio_default_fixture_loop_scope = "function"
 asyncio_mode = "auto"
+
+[tool.codespell]
+skip = "docs/*,*.html,examples/*, *.pyi"
+ignore-words-list = "te, TreeE"

+ 5 - 1
reflex/.templates/jinja/web/pages/stateful_component.js.jinja2

@@ -5,11 +5,15 @@ export function {{tag_name}} () {
   {{ hook }}
   {% endfor %}
 
+  {% for hook, data in component._get_all_hooks().items() if not data.position or data.position == const.hook_position.PRE_TRIGGER %}
+  {{ hook }}
+  {% endfor %}
+
   {% for hook in memo_trigger_hooks %}
   {{ hook }}
   {% endfor %}
 
-  {% for hook in component._get_all_hooks() %}
+  {% for hook, data in component._get_all_hooks().items() if data.position and data.position == const.hook_position.POST_TRIGGER %}
   {{ hook }}
   {% endfor %}
 

+ 1 - 1
reflex/.templates/jinja/web/utils/context.js.jinja2

@@ -28,7 +28,7 @@ export const state_name = "{{state_name}}"
 
 export const exception_state_name = "{{const.frontend_exception_state}}"
 
-// Theses events are triggered on initial load and each page navigation.
+// These events are triggered on initial load and each page navigation.
 export const onLoadInternalEvent = () => {
     const internal_events = [];
 

+ 35 - 27
reflex/.templates/web/utils/state.js

@@ -40,9 +40,6 @@ let event_processing = false;
 // Array holding pending events to be processed.
 const event_queue = [];
 
-// Pending upload promises, by id
-const upload_controllers = {};
-
 /**
  * Generate a UUID (Used for session tokens).
  * Taken from: https://stackoverflow.com/questions/105034/how-do-i-create-a-guid-uuid
@@ -300,7 +297,7 @@ export const applyEvent = async (event, socket) => {
   if (socket) {
     socket.emit(
       "event",
-      JSON.stringify(event, (k, v) => (v === undefined ? null : v))
+      event,
     );
     return true;
   }
@@ -407,6 +404,8 @@ export const connect = async (
     transports: transports,
     autoUnref: false,
   });
+  // Ensure undefined fields in events are sent as null instead of removed
+  socket.current.io.encoder.replacer = (k, v) => (v === undefined ? null : v)
 
   function checkVisibility() {
     if (document.visibilityState === "visible") {
@@ -443,8 +442,7 @@ export const connect = async (
   });
 
   // On each received message, queue the updates and events.
-  socket.current.on("event", async (message) => {
-    const update = JSON5.parse(message);
+  socket.current.on("event", async (update) => {
     for (const substate in update.delta) {
       dispatch[substate](update.delta[substate]);
     }
@@ -456,7 +454,7 @@ export const connect = async (
   });
   socket.current.on("reload", async (event) => {
     event_processing = false;
-    queueEvents([...initialEvents(), JSON5.parse(event)], socket);
+    queueEvents([...initialEvents(), event], socket);
   });
 
   document.addEventListener("visibilitychange", checkVisibility);
@@ -485,7 +483,9 @@ export const uploadFiles = async (
     return false;
   }
 
-  if (upload_controllers[upload_id]) {
+  const upload_ref_name = `__upload_controllers_${upload_id}`
+
+  if (refs[upload_ref_name]) {
     console.log("Upload already in progress for ", upload_id);
     return false;
   }
@@ -497,23 +497,31 @@ export const uploadFiles = async (
     // Whenever called, responseText will contain the entire response so far.
     const chunks = progressEvent.event.target.responseText.trim().split("\n");
     // So only process _new_ chunks beyond resp_idx.
-    chunks.slice(resp_idx).map((chunk) => {
-      event_callbacks.map((f, ix) => {
-        f(chunk)
-          .then(() => {
-            if (ix === event_callbacks.length - 1) {
-              // Mark this chunk as processed.
-              resp_idx += 1;
-            }
-          })
-          .catch((e) => {
-            if (progressEvent.progress === 1) {
-              // Chunk may be incomplete, so only report errors when full response is available.
-              console.log("Error parsing chunk", chunk, e);
-            }
-            return;
-          });
-      });
+    chunks.slice(resp_idx).map((chunk_json) => {
+      try {
+        const chunk = JSON5.parse(chunk_json);
+        event_callbacks.map((f, ix) => {
+          f(chunk)
+            .then(() => {
+              if (ix === event_callbacks.length - 1) {
+                // Mark this chunk as processed.
+                resp_idx += 1;
+              }
+            })
+            .catch((e) => {
+              if (progressEvent.progress === 1) {
+                // Chunk may be incomplete, so only report errors when full response is available.
+                console.log("Error processing chunk", chunk, e);
+              }
+              return;
+            });
+        });
+      } catch (e) {
+        if (progressEvent.progress === 1) {
+          console.log("Error parsing chunk", chunk_json, e);
+        }
+        return;
+      }
     });
   };
 
@@ -537,7 +545,7 @@ export const uploadFiles = async (
   });
 
   // Send the file to the server.
-  upload_controllers[upload_id] = controller;
+  refs[upload_ref_name] = controller;
 
   try {
     return await axios.post(getBackendURL(UPLOADURL), formdata, config);
@@ -557,7 +565,7 @@ export const uploadFiles = async (
     }
     return false;
   } finally {
-    delete upload_controllers[upload_id];
+    delete refs[upload_ref_name];
   }
 };
 

+ 21 - 14
reflex/app.py

@@ -17,6 +17,7 @@ import sys
 import traceback
 from datetime import datetime
 from pathlib import Path
+from types import SimpleNamespace
 from typing import (
     TYPE_CHECKING,
     Any,
@@ -363,6 +364,10 @@ class App(MiddlewareMixin, LifespanMixin):
                 max_http_buffer_size=constants.POLLING_MAX_HTTP_BUFFER_SIZE,
                 ping_interval=constants.Ping.INTERVAL,
                 ping_timeout=constants.Ping.TIMEOUT,
+                json=SimpleNamespace(
+                    dumps=staticmethod(format.json_dumps),
+                    loads=staticmethod(json.loads),
+                ),
                 transports=["websocket"],
             )
         elif getattr(self.sio, "async_mode", "") != "asgi":
@@ -431,7 +436,7 @@ class App(MiddlewareMixin, LifespanMixin):
             allow_credentials=True,
             allow_methods=["*"],
             allow_headers=["*"],
-            allow_origins=["*"],
+            allow_origins=get_config().cors_allowed_origins,
         )
 
     @property
@@ -1290,7 +1295,7 @@ async def process(
                 await asyncio.create_task(
                     app.event_namespace.emit(
                         "reload",
-                        data=format.json_dumps(event),
+                        data=event,
                         to=sid,
                     )
                 )
@@ -1351,20 +1356,22 @@ async def health() -> JSONResponse:
     health_status = {"status": True}
     status_code = 200
 
-    db_status, redis_status = await asyncio.gather(
-        get_db_status(), prerequisites.get_redis_status()
-    )
+    tasks = []
+
+    if prerequisites.check_db_used():
+        tasks.append(get_db_status())
+    if prerequisites.check_redis_used():
+        tasks.append(prerequisites.get_redis_status())
 
-    health_status["db"] = db_status
+    results = await asyncio.gather(*tasks)
 
-    if redis_status is None:
+    for result in results:
+        health_status |= result
+
+    if "redis" in health_status and health_status["redis"] is None:
         health_status["redis"] = False
-    else:
-        health_status["redis"] = redis_status
 
-    if not health_status["db"] or (
-        not health_status["redis"] and redis_status is not None
-    ):
+    if not all(health_status.values()):
         health_status["status"] = False
         status_code = 503
 
@@ -1543,7 +1550,7 @@ class EventNamespace(AsyncNamespace):
         """
         # Creating a task prevents the update from being blocked behind other coroutines.
         await asyncio.create_task(
-            self.emit(str(constants.SocketEvent.EVENT), update.json(), to=sid)
+            self.emit(str(constants.SocketEvent.EVENT), update, to=sid)
         )
 
     async def on_event(self, sid, data):
@@ -1556,7 +1563,7 @@ class EventNamespace(AsyncNamespace):
             sid: The Socket.IO session id.
             data: The event data.
         """
-        fields = json.loads(data)
+        fields = data
         # Get the event.
         event = Event(
             **{k: v for k, v in fields.items() if k not in ("handler", "event_actions")}

+ 8 - 7
reflex/base.py

@@ -30,15 +30,16 @@ def validate_field_name(bases: List[Type["BaseModel"]], field_name: str) -> None
 
     # can't use reflex.config.environment here cause of circular import
     reload = os.getenv("__RELOAD_CONFIG", "").lower() == "true"
-    for base in bases:
-        try:
+    base = None
+    try:
+        for base in bases:
             if not reload and getattr(base, field_name, None):
                 pass
-        except TypeError as te:
-            raise VarNameError(
-                f'State var "{field_name}" in {base} has been shadowed by a substate var; '
-                f'use a different field name instead".'
-            ) from te
+    except TypeError as te:
+        raise VarNameError(
+            f'State var "{field_name}" in {base} has been shadowed by a substate var; '
+            f'use a different field name instead".'
+        ) from te
 
 
 # monkeypatch pydantic validate_field_name method to skip validating

+ 1 - 0
reflex/compiler/templates.py

@@ -45,6 +45,7 @@ class ReflexJinjaEnvironment(Environment):
             "on_load_internal": constants.CompileVars.ON_LOAD_INTERNAL,
             "update_vars_internal": constants.CompileVars.UPDATE_VARS_INTERNAL,
             "frontend_exception_state": constants.CompileVars.FRONTEND_EXCEPTION_STATE_FULL,
+            "hook_position": constants.Hooks.HookPosition,
         }
 
 

+ 2 - 3
reflex/compiler/utils.py

@@ -115,7 +115,7 @@ def compile_imports(import_dict: ParsedImportDict) -> list[dict]:
         default, rest = compile_import_statement(fields)
 
         # prevent lib from being rendered on the page if all imports are non rendered kind
-        if not any({f.render for f in fields}):  # type: ignore
+        if not any(f.render for f in fields):  # type: ignore
             continue
 
         if not lib:
@@ -123,8 +123,7 @@ def compile_imports(import_dict: ParsedImportDict) -> list[dict]:
                 raise ValueError("No default field allowed for empty library.")
             if rest is None or len(rest) == 0:
                 raise ValueError("No fields to import.")
-            for module in sorted(rest):
-                import_dicts.append(get_import_dict(module))
+            import_dicts.extend(get_import_dict(module) for module in sorted(rest))
             continue
 
         # remove the version before rendering the package imports

+ 47 - 23
reflex/components/component.py

@@ -1208,7 +1208,7 @@ class Component(BaseComponent, ABC):
         Yields:
             The parent classes that define the method (differently than the base).
         """
-        seen_methods = set([getattr(Component, method)])
+        seen_methods = {getattr(Component, method)}
         for clz in cls.mro():
             if clz is Component:
                 break
@@ -1368,7 +1368,9 @@ class Component(BaseComponent, ABC):
         if user_hooks_data is not None:
             other_imports.append(user_hooks_data.imports)
         other_imports.extend(
-            hook_imports for hook_imports in self._get_added_hooks().values()
+            hook_vardata.imports
+            for hook_vardata in self._get_added_hooks().values()
+            if hook_vardata is not None
         )
 
         return imports.merge_imports(_imports, *other_imports)
@@ -1390,15 +1392,9 @@ class Component(BaseComponent, ABC):
 
         # Collect imports from Vars used directly by this component.
         var_datas = [var._get_all_var_data() for var in self._get_vars()]
-        var_imports: List[ImmutableParsedImportDict] = list(
-            map(
-                lambda var_data: var_data.imports,
-                filter(
-                    None,
-                    var_datas,
-                ),
-            )
-        )
+        var_imports: List[ImmutableParsedImportDict] = [
+            var_data.imports for var_data in var_datas if var_data is not None
+        ]
 
         added_import_dicts: list[ParsedImportDict] = []
         for clz in self._iter_parent_classes_with_method("add_imports"):
@@ -1407,8 +1403,9 @@ class Component(BaseComponent, ABC):
             if not isinstance(list_of_import_dict, list):
                 list_of_import_dict = [list_of_import_dict]
 
-            for import_dict in list_of_import_dict:
-                added_import_dicts.append(parse_imports(import_dict))
+            added_import_dicts.extend(
+                [parse_imports(import_dict) for import_dict in list_of_import_dict]
+            )
 
         return imports.merge_imports(
             *self._get_props_imports(),
@@ -1522,7 +1519,7 @@ class Component(BaseComponent, ABC):
             **self._get_special_hooks(),
         }
 
-    def _get_added_hooks(self) -> dict[str, ImportDict]:
+    def _get_added_hooks(self) -> dict[str, VarData | None]:
         """Get the hooks added via `add_hooks` method.
 
         Returns:
@@ -1531,17 +1528,15 @@ class Component(BaseComponent, ABC):
         code = {}
 
         def extract_var_hooks(hook: Var):
-            _imports = {}
             var_data = VarData.merge(hook._get_all_var_data())
             if var_data is not None:
                 for sub_hook in var_data.hooks:
-                    code[sub_hook] = {}
-                if var_data.imports:
-                    _imports = var_data.imports
+                    code[sub_hook] = None
+
             if str(hook) in code:
-                code[str(hook)] = imports.merge_imports(code[str(hook)], _imports)
+                code[str(hook)] = VarData.merge(var_data, code[str(hook)])
             else:
-                code[str(hook)] = _imports
+                code[str(hook)] = var_data
 
         # Add the hook code from add_hooks for each parent class (this is reversed to preserve
         # the order of the hooks in the final output)
@@ -1550,7 +1545,7 @@ class Component(BaseComponent, ABC):
                 if isinstance(hook, Var):
                     extract_var_hooks(hook)
                 else:
-                    code[hook] = {}
+                    code[hook] = None
 
         return code
 
@@ -1592,8 +1587,7 @@ class Component(BaseComponent, ABC):
         if hooks is not None:
             code[hooks] = None
 
-        for hook in self._get_added_hooks():
-            code[hook] = None
+        code.update(self._get_added_hooks())
 
         # Add the hook code for the children.
         for child in self.children:
@@ -2195,6 +2189,31 @@ class StatefulComponent(BaseComponent):
             ]
         return [var_name]
 
+    @staticmethod
+    def _get_deps_from_event_trigger(event: EventChain | EventSpec | Var) -> set[str]:
+        """Get the dependencies accessed by event triggers.
+
+        Args:
+            event: The event trigger to extract deps from.
+
+        Returns:
+            The dependencies accessed by the event triggers.
+        """
+        events: list = [event]
+        deps = set()
+
+        if isinstance(event, EventChain):
+            events.extend(event.events)
+
+        for ev in events:
+            if isinstance(ev, EventSpec):
+                for arg in ev.args:
+                    for a in arg:
+                        var_datas = VarData.merge(a._get_all_var_data())
+                        if var_datas and var_datas.deps is not None:
+                            deps |= {str(dep) for dep in var_datas.deps}
+        return deps
+
     @classmethod
     def _get_memoized_event_triggers(
         cls,
@@ -2231,6 +2250,11 @@ class StatefulComponent(BaseComponent):
 
             # Calculate Var dependencies accessed by the handler for useCallback dep array.
             var_deps = ["addEvents", "Event"]
+
+            # Get deps from event trigger var data.
+            var_deps.extend(cls._get_deps_from_event_trigger(event))
+
+            # Get deps from hooks.
             for arg in event_args:
                 var_data = arg._get_all_var_data()
                 if var_data is None:

+ 1 - 1
reflex/components/core/banner.py

@@ -241,7 +241,7 @@ class WifiOffPulse(Icon):
             size=props.pop("size", 32),
             z_index=props.pop("z_index", 9999),
             position=props.pop("position", "fixed"),
-            bottom=props.pop("botton", "33px"),
+            bottom=props.pop("bottom", "33px"),
             right=props.pop("right", "33px"),
             animation=LiteralVar.create(f"{pulse_var} 1s infinite"),
             **props,

+ 1 - 1
reflex/components/core/breakpoints.py

@@ -58,7 +58,7 @@ class Breakpoints(Dict[K, V]):
 
         Args:
             custom: Custom mapping using CSS values or variables.
-            initial: Styling when in the inital width
+            initial: Styling when in the initial width
             xs: Styling when in the extra-small width
             sm: Styling when in the small width
             md: Styling when in the medium width

+ 10 - 8
reflex/components/core/clipboard.py

@@ -6,11 +6,12 @@ from typing import Dict, List, Tuple, Union
 
 from reflex.components.base.fragment import Fragment
 from reflex.components.tags.tag import Tag
+from reflex.constants.compiler import Hooks
 from reflex.event import EventChain, EventHandler, passthrough_event_spec
 from reflex.utils.format import format_prop, wrap
 from reflex.utils.imports import ImportVar
 from reflex.vars import get_unique_variable_name
-from reflex.vars.base import Var
+from reflex.vars.base import Var, VarData
 
 
 class Clipboard(Fragment):
@@ -72,7 +73,7 @@ class Clipboard(Fragment):
             ),
         }
 
-    def add_hooks(self) -> list[str]:
+    def add_hooks(self) -> list[str | Var[str]]:
         """Add hook to register paste event listener.
 
         Returns:
@@ -83,13 +84,14 @@ class Clipboard(Fragment):
             return []
         if isinstance(on_paste, EventChain):
             on_paste = wrap(str(format_prop(on_paste)).strip("{}"), "(")
+        hook_expr = f"usePasteHandler({self.targets!s}, {self.on_paste_event_actions!s}, {on_paste!s})"
+
         return [
-            "usePasteHandler(%s, %s, %s)"
-            % (
-                str(self.targets),
-                str(self.on_paste_event_actions),
-                on_paste,
-            )
+            Var(
+                hook_expr,
+                _var_type="str",
+                _var_data=VarData(position=Hooks.HookPosition.POST_TRIGGER),
+            ),
         ]
 
 

+ 1 - 1
reflex/components/core/clipboard.pyi

@@ -71,6 +71,6 @@ class Clipboard(Fragment):
         ...
 
     def add_imports(self) -> dict[str, ImportVar]: ...
-    def add_hooks(self) -> list[str]: ...
+    def add_hooks(self) -> list[str | Var[str]]: ...
 
 clipboard = Clipboard.create

+ 5 - 3
reflex/components/core/upload.py

@@ -29,7 +29,7 @@ from reflex.event import (
 from reflex.utils import format
 from reflex.utils.imports import ImportVar
 from reflex.vars import VarData
-from reflex.vars.base import CallableVar, LiteralVar, Var, get_unique_variable_name
+from reflex.vars.base import CallableVar, Var, get_unique_variable_name
 from reflex.vars.sequence import LiteralStringVar
 
 DEFAULT_UPLOAD_ID: str = "default"
@@ -108,7 +108,8 @@ def clear_selected_files(id_: str = DEFAULT_UPLOAD_ID) -> EventSpec:
     # UploadFilesProvider assigns a special function to clear selected files
     # into the shared global refs object to make it accessible outside a React
     # component via `run_script` (otherwise backend could never clear files).
-    return run_script(f"refs['__clear_selected_files']({id_!r})")
+    func = Var("__clear_selected_files")._as_ref()
+    return run_script(f"{func}({id_!r})")
 
 
 def cancel_upload(upload_id: str) -> EventSpec:
@@ -120,7 +121,8 @@ def cancel_upload(upload_id: str) -> EventSpec:
     Returns:
         An event spec that cancels the upload when triggered.
     """
-    return run_script(f"upload_controllers[{LiteralVar.create(upload_id)!s}]?.abort()")
+    controller = Var(f"__upload_controllers_{upload_id}")._as_ref()
+    return run_script(f"{controller}?.abort()")
 
 
 def get_upload_dir() -> Path:

+ 1 - 1
reflex/components/datadisplay/code.py

@@ -445,7 +445,7 @@ class CodeBlock(Component, MarkdownComponentMap):
                 dark=Theme.one_dark,
             )
 
-        # react-syntax-highlighter doesnt have an explicit "light" or "dark" theme so we use one-light and one-dark
+        # react-syntax-highlighter doesn't have an explicit "light" or "dark" theme so we use one-light and one-dark
         # themes respectively to ensure code compatibility.
         if "theme" in props and not isinstance(props["theme"], Var):
             props["theme"] = getattr(Theme, format.to_snake_case(props["theme"]))  # type: ignore

+ 6 - 3
reflex/components/datadisplay/dataeditor.py

@@ -219,7 +219,7 @@ class DataEditor(NoSSRComponent):
     # The minimum width a column can be resized to.
     min_column_width: Var[int]
 
-    # Determins the height of each row.
+    # Determines the height of each row.
     row_height: Var[int]
 
     # Kind of row markers.
@@ -339,8 +339,11 @@ class DataEditor(NoSSRComponent):
         editor_id = get_unique_variable_name()
 
         # Define the name of the getData callback associated with this component and assign to get_cell_content.
-        data_callback = f"getData_{editor_id}"
-        self.get_cell_content = Var(_js_expr=data_callback)  # type: ignore
+        if self.get_cell_content is not None:
+            data_callback = self.get_cell_content._js_expr
+        else:
+            data_callback = f"getData_{editor_id}"
+            self.get_cell_content = Var(_js_expr=data_callback)  # type: ignore
 
         code = [f"function {data_callback}([col, row])" "{"]
 

+ 1 - 1
reflex/components/datadisplay/dataeditor.pyi

@@ -291,7 +291,7 @@ class DataEditor(NoSSRComponent):
             max_column_auto_width: The maximum width a column can be automatically sized to.
             max_column_width: The maximum width a column can be resized to.
             min_column_width: The minimum width a column can be resized to.
-            row_height: Determins the height of each row.
+            row_height: Determines the height of each row.
             row_markers: Kind of row markers.
             row_marker_start_index: Changes the starting index for row markers.
             row_marker_width: Sets the width of row markers in pixels, if unset row markers will automatically size.

+ 1 - 1
reflex/components/el/elements/__init__.py

@@ -127,7 +127,7 @@ _MAPPING = {
 
 
 EXCLUDE = ["del_", "Del", "image"]
-for _, v in _MAPPING.items():
+for v in _MAPPING.values():
     v.extend([mod.capitalize() for mod in v if mod not in EXCLUDE])
 
 _SUBMOD_ATTRS: dict[str, list[str]] = _MAPPING

+ 1 - 1
reflex/components/el/elements/__init__.pyi

@@ -339,5 +339,5 @@ _MAPPING = {
     ],
 }
 EXCLUDE = ["del_", "Del", "image"]
-for _, v in _MAPPING.items():
+for v in _MAPPING.values():
     v.extend([mod.capitalize() for mod in v if mod not in EXCLUDE])

+ 28 - 0
reflex/components/el/elements/forms.py

@@ -18,6 +18,7 @@ from reflex.event import (
     prevent_default,
 )
 from reflex.utils.imports import ImportDict
+from reflex.utils.types import is_optional
 from reflex.vars import VarData
 from reflex.vars.base import LiteralVar, Var
 
@@ -382,6 +383,33 @@ class Input(BaseHTML):
     # Fired when a key is released
     on_key_up: EventHandler[key_event]
 
+    @classmethod
+    def create(cls, *children, **props):
+        """Create an Input component.
+
+        Args:
+            *children: The children of the component.
+            **props: The properties of the component.
+
+        Returns:
+            The component.
+        """
+        from reflex.vars.number import ternary_operation
+
+        value = props.get("value")
+
+        # React expects an empty string(instead of null) for controlled inputs.
+        if value is not None and is_optional(
+            (value_var := Var.create(value))._var_type
+        ):
+            props["value"] = ternary_operation(
+                (value_var != Var.create(None))  # pyright: ignore [reportGeneralTypeIssues]
+                & (value_var != Var(_js_expr="undefined")),
+                value,
+                Var.create(""),
+            )
+        return super().create(*children, **props)
+
 
 class Label(BaseHTML):
     """Display the label element."""

+ 2 - 2
reflex/components/el/elements/forms.pyi

@@ -512,7 +512,7 @@ class Input(BaseHTML):
         on_unmount: Optional[EventType[[], BASE_STATE]] = None,
         **props,
     ) -> "Input":
-        """Create the component.
+        """Create an Input component.
 
         Args:
             *children: The children of the component.
@@ -576,7 +576,7 @@ class Input(BaseHTML):
             class_name: The class name for the component.
             autofocus: Whether the component should take the focus once the page is loaded
             custom_attrs: custom attribute
-            **props: The props of the component.
+            **props: The properties of the component.
 
         Returns:
             The component.

+ 1 - 1
reflex/components/el/elements/metadata.py

@@ -81,7 +81,7 @@ class Title(Element):
     tag = "title"
 
 
-# Had to be named with an underscore so it doesnt conflict with reflex.style Style in pyi
+# Had to be named with an underscore so it doesn't conflict with reflex.style Style in pyi
 class StyleEl(Element):
     """Display the style element."""
 

+ 3 - 3
reflex/components/plotly/plotly.py

@@ -149,10 +149,10 @@ class Plotly(NoSSRComponent):
     # Fired when a plot element is hovered over.
     on_hover: EventHandler[_event_points_data_signature]
 
-    # Fired after the plot is layed out (zoom, pan, etc).
+    # Fired after the plot is laid out (zoom, pan, etc).
     on_relayout: EventHandler[no_args_event_spec]
 
-    # Fired while the plot is being layed out.
+    # Fired while the plot is being laid out.
     on_relayouting: EventHandler[no_args_event_spec]
 
     # Fired after the plot style is changed.
@@ -167,7 +167,7 @@ class Plotly(NoSSRComponent):
     # Fired while dragging a selection.
     on_selecting: EventHandler[_event_points_data_signature]
 
-    # Fired while an animation is occuring.
+    # Fired while an animation is occurring.
     on_transitioning: EventHandler[no_args_event_spec]
 
     # Fired when a transition is stopped early.

+ 3 - 3
reflex/components/plotly/plotly.pyi

@@ -130,13 +130,13 @@ class Plotly(NoSSRComponent):
             on_deselect: Fired when a selection is cleared (via double click).
             on_double_click: Fired when the plot is double clicked.
             on_hover: Fired when a plot element is hovered over.
-            on_relayout: Fired after the plot is layed out (zoom, pan, etc).
-            on_relayouting: Fired while the plot is being layed out.
+            on_relayout: Fired after the plot is laid out (zoom, pan, etc).
+            on_relayouting: Fired while the plot is being laid out.
             on_restyle: Fired after the plot style is changed.
             on_redraw: Fired after the plot is redrawn.
             on_selected: Fired after selecting plot elements.
             on_selecting: Fired while dragging a selection.
-            on_transitioning: Fired while an animation is occuring.
+            on_transitioning: Fired while an animation is occurring.
             on_transition_interrupted: Fired when a transition is stopped early.
             on_unhover: Fired when a hovered element is no longer hovered.
             style: The style of the component.

+ 1 - 1
reflex/components/radix/primitives/slider.py

@@ -34,7 +34,7 @@ def on_value_event_spec(
 
 
 class SliderRoot(SliderComponent):
-    """The Slider component comtaining all slider parts."""
+    """The Slider component containing all slider parts."""
 
     tag = "Root"
     alias = "RadixSliderRoot"

+ 11 - 0
reflex/components/radix/themes/components/context_menu.py

@@ -8,6 +8,7 @@ from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spe
 from reflex.vars.base import Var
 
 from ..base import LiteralAccentColor, RadixThemesComponent
+from .checkbox import Checkbox
 
 LiteralDirType = Literal["ltr", "rtl"]
 
@@ -232,6 +233,15 @@ class ContextMenuSeparator(RadixThemesComponent):
     tag = "ContextMenu.Separator"
 
 
+class ContextMenuCheckbox(Checkbox):
+    """The component that contains the checkbox."""
+
+    tag = "ContextMenu.CheckboxItem"
+
+    # Text to render as shortcut.
+    shortcut: Var[str]
+
+
 class ContextMenu(ComponentNamespace):
     """Menu representing a set of actions, displayed at the origin of a pointer right-click or long-press."""
 
@@ -243,6 +253,7 @@ class ContextMenu(ComponentNamespace):
     sub_content = staticmethod(ContextMenuSubContent.create)
     item = staticmethod(ContextMenuItem.create)
     separator = staticmethod(ContextMenuSeparator.create)
+    checkbox = staticmethod(ContextMenuCheckbox.create)
 
 
 context_menu = ContextMenu()

+ 155 - 0
reflex/components/radix/themes/components/context_menu.pyi

@@ -12,6 +12,7 @@ from reflex.style import Style
 from reflex.vars.base import Var
 
 from ..base import RadixThemesComponent
+from .checkbox import Checkbox
 
 LiteralDirType = Literal["ltr", "rtl"]
 LiteralSizeType = Literal["1", "2"]
@@ -672,6 +673,159 @@ class ContextMenuSeparator(RadixThemesComponent):
         """
         ...
 
+class ContextMenuCheckbox(Checkbox):
+    @overload
+    @classmethod
+    def create(  # type: ignore
+        cls,
+        *children,
+        shortcut: Optional[Union[Var[str], str]] = None,
+        as_child: Optional[Union[Var[bool], bool]] = None,
+        size: Optional[
+            Union[
+                Breakpoints[str, Literal["1", "2", "3"]],
+                Literal["1", "2", "3"],
+                Var[
+                    Union[
+                        Breakpoints[str, Literal["1", "2", "3"]], Literal["1", "2", "3"]
+                    ]
+                ],
+            ]
+        ] = None,
+        variant: Optional[
+            Union[
+                Literal["classic", "soft", "surface"],
+                Var[Literal["classic", "soft", "surface"]],
+            ]
+        ] = None,
+        color_scheme: Optional[
+            Union[
+                Literal[
+                    "amber",
+                    "blue",
+                    "bronze",
+                    "brown",
+                    "crimson",
+                    "cyan",
+                    "gold",
+                    "grass",
+                    "gray",
+                    "green",
+                    "indigo",
+                    "iris",
+                    "jade",
+                    "lime",
+                    "mint",
+                    "orange",
+                    "pink",
+                    "plum",
+                    "purple",
+                    "red",
+                    "ruby",
+                    "sky",
+                    "teal",
+                    "tomato",
+                    "violet",
+                    "yellow",
+                ],
+                Var[
+                    Literal[
+                        "amber",
+                        "blue",
+                        "bronze",
+                        "brown",
+                        "crimson",
+                        "cyan",
+                        "gold",
+                        "grass",
+                        "gray",
+                        "green",
+                        "indigo",
+                        "iris",
+                        "jade",
+                        "lime",
+                        "mint",
+                        "orange",
+                        "pink",
+                        "plum",
+                        "purple",
+                        "red",
+                        "ruby",
+                        "sky",
+                        "teal",
+                        "tomato",
+                        "violet",
+                        "yellow",
+                    ]
+                ],
+            ]
+        ] = None,
+        high_contrast: Optional[Union[Var[bool], bool]] = None,
+        default_checked: Optional[Union[Var[bool], bool]] = None,
+        checked: Optional[Union[Var[bool], bool]] = None,
+        disabled: Optional[Union[Var[bool], bool]] = None,
+        required: Optional[Union[Var[bool], bool]] = None,
+        name: Optional[Union[Var[str], str]] = None,
+        value: Optional[Union[Var[str], 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_change: Optional[
+            Union[EventType[[], BASE_STATE], EventType[[bool], 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,
+    ) -> "ContextMenuCheckbox":
+        """Create a new component instance.
+
+        Will prepend "RadixThemes" to the component tag to avoid conflicts with
+        other UI libraries for common names, like Text and Button.
+
+        Args:
+            *children: Child components.
+            shortcut: Text to render as shortcut.
+            as_child: Change the default rendered element for the one passed as a child, merging their props and behavior.
+            size: Checkbox size "1" - "3"
+            variant: Variant of checkbox: "classic" | "surface" | "soft"
+            color_scheme: Override theme color for checkbox
+            high_contrast: Whether to render the checkbox with higher contrast color against background
+            default_checked: Whether the checkbox is checked by default
+            checked: Whether the checkbox is checked
+            disabled: Whether the checkbox is disabled
+            required: Whether the checkbox is required
+            name: The name of the checkbox control when submitting the form.
+            value: The value of the checkbox control when submitting the form.
+            on_change: Fired when the checkbox is checked or unchecked.
+            style: The style of the component.
+            key: A unique key for the component.
+            id: The id for the component.
+            class_name: The class name for the component.
+            autofocus: Whether the component should take the focus once the page is loaded
+            custom_attrs: custom attribute
+            **props: Component properties.
+
+        Returns:
+            A new component instance.
+        """
+        ...
+
 class ContextMenu(ComponentNamespace):
     root = staticmethod(ContextMenuRoot.create)
     trigger = staticmethod(ContextMenuTrigger.create)
@@ -681,5 +835,6 @@ class ContextMenu(ComponentNamespace):
     sub_content = staticmethod(ContextMenuSubContent.create)
     item = staticmethod(ContextMenuItem.create)
     separator = staticmethod(ContextMenuSeparator.create)
+    checkbox = staticmethod(ContextMenuCheckbox.create)
 
 context_menu = ContextMenu()

+ 1 - 1
reflex/components/radix/themes/components/icon_button.py

@@ -79,7 +79,7 @@ class IconButton(elements.Button, RadixLoadingProp, RadixThemesComponent):
             else:
                 size_map_var = Match.create(
                     props["size"],
-                    *[(size, px) for size, px in RADIX_TO_LUCIDE_SIZE.items()],
+                    *list(RADIX_TO_LUCIDE_SIZE.items()),
                     12,
                 )
                 if not isinstance(size_map_var, Var):

+ 16 - 1
reflex/components/radix/themes/components/text_field.py

@@ -9,7 +9,9 @@ from reflex.components.core.breakpoints import Responsive
 from reflex.components.core.debounce import DebounceInput
 from reflex.components.el import elements
 from reflex.event import EventHandler, input_event, key_event
+from reflex.utils.types import is_optional
 from reflex.vars.base import Var
+from reflex.vars.number import ternary_operation
 
 from ..base import LiteralAccentColor, LiteralRadius, RadixThemesComponent
 
@@ -17,7 +19,7 @@ LiteralTextFieldSize = Literal["1", "2", "3"]
 LiteralTextFieldVariant = Literal["classic", "surface", "soft"]
 
 
-class TextFieldRoot(elements.Div, RadixThemesComponent):
+class TextFieldRoot(elements.Input, RadixThemesComponent):
     """Captures user input with an optional slot for buttons and icons."""
 
     tag = "TextField.Root"
@@ -96,6 +98,19 @@ class TextFieldRoot(elements.Div, RadixThemesComponent):
         Returns:
             The component.
         """
+        value = props.get("value")
+
+        # React expects an empty string(instead of null) for controlled inputs.
+        if value is not None and is_optional(
+            (value_var := Var.create(value))._var_type
+        ):
+            props["value"] = ternary_operation(
+                (value_var != Var.create(None))  # pyright: ignore [reportGeneralTypeIssues]
+                & (value_var != Var(_js_expr="undefined")),
+                value,
+                Var.create(""),
+            )
+
         component = super().create(*children, **props)
         if props.get("value") is not None and props.get("on_change") is not None:
             # create a debounced input if the user requests full control to avoid typing jank

+ 103 - 15
reflex/components/radix/themes/components/text_field.pyi

@@ -17,7 +17,7 @@ from ..base import RadixThemesComponent
 LiteralTextFieldSize = Literal["1", "2", "3"]
 LiteralTextFieldVariant = Literal["classic", "surface", "soft"]
 
-class TextFieldRoot(elements.Div, RadixThemesComponent):
+class TextFieldRoot(elements.Input, RadixThemesComponent):
     @overload
     @classmethod
     def create(  # type: ignore
@@ -120,6 +120,30 @@ class TextFieldRoot(elements.Div, RadixThemesComponent):
         type: Optional[Union[Var[str], str]] = None,
         value: Optional[Union[Var[Union[float, int, str]], float, int, str]] = None,
         list: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        accept: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        alt: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        auto_focus: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        capture: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        checked: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        default_checked: Optional[Union[Var[bool], bool]] = None,
+        dirname: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        form: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        form_action: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        form_enc_type: Optional[
+            Union[Var[Union[bool, int, str]], bool, int, str]
+        ] = None,
+        form_method: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        form_no_validate: Optional[
+            Union[Var[Union[bool, int, str]], bool, int, str]
+        ] = None,
+        form_target: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        max: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        min: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        multiple: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        pattern: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        src: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        step: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        use_map: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
         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]
@@ -192,12 +216,12 @@ class TextFieldRoot(elements.Div, RadixThemesComponent):
 
         Args:
             *children: The children of the component.
-            size: Text field size "1" - "3"
+            size: Specifies the visible width of a text control
             variant: Variant of text field: "classic" | "surface" | "soft"
             color_scheme: Override theme color for text field
             radius: Override theme radius for text field: "none" | "small" | "medium" | "large" | "full"
             auto_complete: Whether the input should have autocomplete enabled
-            default_value: The value of the input when initially rendered.
+            default_value: The initial value for a text field
             disabled: Disables the input
             max_length: Specifies the maximum number of characters allowed in the input
             min_length: Specifies the minimum number of characters required in the input
@@ -208,11 +232,31 @@ class TextFieldRoot(elements.Div, RadixThemesComponent):
             type: Specifies the type of input
             value: Value of the input
             list: References a datalist for suggested options
-            on_change: Fired when the value of the textarea changes.
-            on_focus: Fired when the textarea is focused.
-            on_blur: Fired when the textarea is blurred.
-            on_key_down: Fired when a key is pressed down.
-            on_key_up: Fired when a key is released.
+            on_change: Fired when the input value changes
+            on_focus: Fired when the input gains focus
+            on_blur: Fired when the input loses focus
+            on_key_down: Fired when a key is pressed down
+            on_key_up: Fired when a key is released
+            accept: Accepted types of files when the input is file type
+            alt: Alternate text for input type="image"
+            auto_focus: Automatically focuses the input when the page loads
+            capture: Captures media from the user (camera or microphone)
+            checked: Indicates whether the input is checked (for checkboxes and radio buttons)
+            default_checked: The initial value (for checkboxes and radio buttons)
+            dirname: Name part of the input to submit in 'dir' and 'name' pair when form is submitted
+            form: Associates the input with a form (by id)
+            form_action: URL to send the form data to (for type="submit" buttons)
+            form_enc_type: How the form data should be encoded when submitting to the server (for type="submit" buttons)
+            form_method: HTTP method to use for sending form data (for type="submit" buttons)
+            form_no_validate: Bypasses form validation when submitting (for type="submit" buttons)
+            form_target: Specifies where to display the response after submitting the form (for type="submit" buttons)
+            max: Specifies the maximum value for the input
+            min: Specifies the minimum value for the input
+            multiple: Indicates whether multiple values can be entered in an input of the type email or file
+            pattern: Regex pattern the input's value must match to be valid
+            src: URL for image inputs
+            step: Specifies the legal number intervals for an input
+            use_map: Name of the image map used with the input
             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.
@@ -457,6 +501,30 @@ class TextField(ComponentNamespace):
         type: Optional[Union[Var[str], str]] = None,
         value: Optional[Union[Var[Union[float, int, str]], float, int, str]] = None,
         list: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        accept: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        alt: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        auto_focus: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        capture: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        checked: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        default_checked: Optional[Union[Var[bool], bool]] = None,
+        dirname: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        form: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        form_action: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        form_enc_type: Optional[
+            Union[Var[Union[bool, int, str]], bool, int, str]
+        ] = None,
+        form_method: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        form_no_validate: Optional[
+            Union[Var[Union[bool, int, str]], bool, int, str]
+        ] = None,
+        form_target: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        max: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        min: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        multiple: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        pattern: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        src: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        step: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
+        use_map: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
         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]
@@ -529,12 +597,12 @@ class TextField(ComponentNamespace):
 
         Args:
             *children: The children of the component.
-            size: Text field size "1" - "3"
+            size: Specifies the visible width of a text control
             variant: Variant of text field: "classic" | "surface" | "soft"
             color_scheme: Override theme color for text field
             radius: Override theme radius for text field: "none" | "small" | "medium" | "large" | "full"
             auto_complete: Whether the input should have autocomplete enabled
-            default_value: The value of the input when initially rendered.
+            default_value: The initial value for a text field
             disabled: Disables the input
             max_length: Specifies the maximum number of characters allowed in the input
             min_length: Specifies the minimum number of characters required in the input
@@ -545,11 +613,31 @@ class TextField(ComponentNamespace):
             type: Specifies the type of input
             value: Value of the input
             list: References a datalist for suggested options
-            on_change: Fired when the value of the textarea changes.
-            on_focus: Fired when the textarea is focused.
-            on_blur: Fired when the textarea is blurred.
-            on_key_down: Fired when a key is pressed down.
-            on_key_up: Fired when a key is released.
+            on_change: Fired when the input value changes
+            on_focus: Fired when the input gains focus
+            on_blur: Fired when the input loses focus
+            on_key_down: Fired when a key is pressed down
+            on_key_up: Fired when a key is released
+            accept: Accepted types of files when the input is file type
+            alt: Alternate text for input type="image"
+            auto_focus: Automatically focuses the input when the page loads
+            capture: Captures media from the user (camera or microphone)
+            checked: Indicates whether the input is checked (for checkboxes and radio buttons)
+            default_checked: The initial value (for checkboxes and radio buttons)
+            dirname: Name part of the input to submit in 'dir' and 'name' pair when form is submitted
+            form: Associates the input with a form (by id)
+            form_action: URL to send the form data to (for type="submit" buttons)
+            form_enc_type: How the form data should be encoded when submitting to the server (for type="submit" buttons)
+            form_method: HTTP method to use for sending form data (for type="submit" buttons)
+            form_no_validate: Bypasses form validation when submitting (for type="submit" buttons)
+            form_target: Specifies where to display the response after submitting the form (for type="submit" buttons)
+            max: Specifies the maximum value for the input
+            min: Specifies the minimum value for the input
+            multiple: Indicates whether multiple values can be entered in an input of the type email or file
+            pattern: Regex pattern the input's value must match to be valid
+            src: URL for image inputs
+            step: Specifies the legal number intervals for an input
+            use_map: Name of the image map used with the input
             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.

+ 1 - 1
reflex/components/radix/themes/layout/center.pyi

@@ -150,7 +150,7 @@ class Center(Flex):
         Args:
             *children: Child components.
             as_child: Change the default rendered element for the one passed as a child, merging their props and behavior.
-            direction: How child items are layed out: "row" | "column" | "row-reverse" | "column-reverse"
+            direction: How child items are laid out: "row" | "column" | "row-reverse" | "column-reverse"
             align: Alignment of children along the main axis: "start" | "center" | "end" | "baseline" | "stretch"
             justify: Alignment of children along the cross axis: "start" | "center" | "end" | "between"
             wrap: Whether children should wrap when they reach the end of their container: "nowrap" | "wrap" | "wrap-reverse"

+ 1 - 1
reflex/components/radix/themes/layout/flex.py

@@ -22,7 +22,7 @@ class Flex(elements.Div, RadixThemesComponent):
     # Change the default rendered element for the one passed as a child, merging their props and behavior.
     as_child: Var[bool]
 
-    # How child items are layed out: "row" | "column" | "row-reverse" | "column-reverse"
+    # How child items are laid out: "row" | "column" | "row-reverse" | "column-reverse"
     direction: Var[Responsive[LiteralFlexDirection]]
 
     # Alignment of children along the main axis: "start" | "center" | "end" | "baseline" | "stretch"

+ 1 - 1
reflex/components/radix/themes/layout/flex.pyi

@@ -153,7 +153,7 @@ class Flex(elements.Div, RadixThemesComponent):
         Args:
             *children: Child components.
             as_child: Change the default rendered element for the one passed as a child, merging their props and behavior.
-            direction: How child items are layed out: "row" | "column" | "row-reverse" | "column-reverse"
+            direction: How child items are laid out: "row" | "column" | "row-reverse" | "column-reverse"
             align: Alignment of children along the main axis: "start" | "center" | "end" | "baseline" | "stretch"
             justify: Alignment of children along the cross axis: "start" | "center" | "end" | "between"
             wrap: Whether children should wrap when they reach the end of their container: "nowrap" | "wrap" | "wrap-reverse"

+ 1 - 1
reflex/components/radix/themes/layout/grid.py

@@ -27,7 +27,7 @@ class Grid(elements.Div, RadixThemesComponent):
     # Number of rows
     rows: Var[Responsive[str]]
 
-    # How the grid items are layed out: "row" | "column" | "dense" | "row-dense" | "column-dense"
+    # How the grid items are laid out: "row" | "column" | "dense" | "row-dense" | "column-dense"
     flow: Var[Responsive[LiteralGridFlow]]
 
     # Alignment of children along the main axis: "start" | "center" | "end" | "baseline" | "stretch"

+ 1 - 1
reflex/components/radix/themes/layout/grid.pyi

@@ -184,7 +184,7 @@ class Grid(elements.Div, RadixThemesComponent):
             as_child: Change the default rendered element for the one passed as a child, merging their props and behavior.
             columns: Number of columns
             rows: Number of rows
-            flow: How the grid items are layed out: "row" | "column" | "dense" | "row-dense" | "column-dense"
+            flow: How the grid items are laid out: "row" | "column" | "dense" | "row-dense" | "column-dense"
             align: Alignment of children along the main axis: "start" | "center" | "end" | "baseline" | "stretch"
             justify: Alignment of children along the cross axis: "start" | "center" | "end" | "between"
             spacing: Gap between children: "0" - "9"

+ 1 - 1
reflex/components/radix/themes/layout/spacer.pyi

@@ -150,7 +150,7 @@ class Spacer(Flex):
         Args:
             *children: Child components.
             as_child: Change the default rendered element for the one passed as a child, merging their props and behavior.
-            direction: How child items are layed out: "row" | "column" | "row-reverse" | "column-reverse"
+            direction: How child items are laid out: "row" | "column" | "row-reverse" | "column-reverse"
             align: Alignment of children along the main axis: "start" | "center" | "end" | "baseline" | "stretch"
             justify: Alignment of children along the cross axis: "start" | "center" | "end" | "between"
             wrap: Whether children should wrap when they reach the end of their container: "nowrap" | "wrap" | "wrap-reverse"

+ 3 - 3
reflex/components/radix/themes/layout/stack.pyi

@@ -126,7 +126,7 @@ class Stack(Flex):
             spacing: Gap between children: "0" - "9"
             align: Alignment of children along the main axis: "start" | "center" | "end" | "baseline" | "stretch"
             as_child: Change the default rendered element for the one passed as a child, merging their props and behavior.
-            direction: How child items are layed out: "row" | "column" | "row-reverse" | "column-reverse"
+            direction: How child items are laid out: "row" | "column" | "row-reverse" | "column-reverse"
             justify: Alignment of children along the cross axis: "start" | "center" | "end" | "between"
             wrap: Whether children should wrap when they reach the end of their container: "nowrap" | "wrap" | "wrap-reverse"
             access_key: Provides a hint for generating a keyboard shortcut for the current element.
@@ -258,7 +258,7 @@ class VStack(Stack):
 
         Args:
             *children: The children of the stack.
-            direction: How child items are layed out: "row" | "column" | "row-reverse" | "column-reverse"
+            direction: How child items are laid out: "row" | "column" | "row-reverse" | "column-reverse"
             spacing: Gap between children: "0" - "9"
             align: Alignment of children along the main axis: "start" | "center" | "end" | "baseline" | "stretch"
             as_child: Change the default rendered element for the one passed as a child, merging their props and behavior.
@@ -393,7 +393,7 @@ class HStack(Stack):
 
         Args:
             *children: The children of the stack.
-            direction: How child items are layed out: "row" | "column" | "row-reverse" | "column-reverse"
+            direction: How child items are laid out: "row" | "column" | "row-reverse" | "column-reverse"
             spacing: Gap between children: "0" - "9"
             align: Alignment of children along the main axis: "start" | "center" | "end" | "baseline" | "stretch"
             as_child: Change the default rendered element for the one passed as a child, merging their props and behavior.

+ 2 - 2
reflex/components/recharts/cartesian.py

@@ -42,7 +42,7 @@ class Axis(Recharts):
     # The width of axis which is usually calculated internally.
     width: Var[Union[str, int]]
 
-    # The height of axis, which can be setted by user.
+    # The height of axis, which can be set by user.
     height: Var[Union[str, int]]
 
     # The type of axis 'number' | 'category'
@@ -60,7 +60,7 @@ class Axis(Recharts):
     # Allow the axis has duplicated categorys or not when the type of axis is "category". Default: True
     allow_duplicated_category: Var[bool]
 
-    # The range of the axis. Work best in conjuction with allow_data_overflow. Default: [0, "auto"]
+    # The range of the axis. Work best in conjunction with allow_data_overflow. Default: [0, "auto"]
     domain: Var[List]
 
     # If set false, no axis line will be drawn. Default: True

+ 6 - 6
reflex/components/recharts/cartesian.pyi

@@ -144,13 +144,13 @@ class Axis(Recharts):
             data_key: The key of data displayed in the axis.
             hide: If set true, the axis do not display in the chart. Default: False
             width: The width of axis which is usually calculated internally.
-            height: The height of axis, which can be setted by user.
+            height: The height of axis, which can be set by user.
             type_: The type of axis 'number' | 'category'
             interval: If set 0, all the ticks will be shown. If set preserveStart", "preserveEnd" or "preserveStartEnd", the ticks which is to be shown or hidden will be calculated automatically. Default: "preserveEnd"
             allow_decimals: Allow the ticks of Axis to be decimals or not. Default: True
             allow_data_overflow: When domain of the axis is specified and the type of the axis is 'number', if allowDataOverflow is set to be false, the domain will be adjusted when the minimum value of data is smaller than domain[0] or the maximum value of data is greater than domain[1] so that the axis displays all data values. If set to true, graphic elements (line, area, bars) will be clipped to conform to the specified domain. Default: False
             allow_duplicated_category: Allow the axis has duplicated categorys or not when the type of axis is "category". Default: True
-            domain: The range of the axis. Work best in conjuction with allow_data_overflow. Default: [0, "auto"]
+            domain: The range of the axis. Work best in conjunction with allow_data_overflow. Default: [0, "auto"]
             axis_line: If set false, no axis line will be drawn. Default: True
             mirror: If set true, flips ticks around the axis line, displaying the labels inside the chart instead of outside. Default: False
             reversed: Reverse the ticks or not. Default: False
@@ -330,13 +330,13 @@ class XAxis(Axis):
             data_key: The key of data displayed in the axis.
             hide: If set true, the axis do not display in the chart. Default: False
             width: The width of axis which is usually calculated internally.
-            height: The height of axis, which can be setted by user.
+            height: The height of axis, which can be set by user.
             type_: The type of axis 'number' | 'category'
             interval: If set 0, all the ticks will be shown. If set preserveStart", "preserveEnd" or "preserveStartEnd", the ticks which is to be shown or hidden will be calculated automatically. Default: "preserveEnd"
             allow_decimals: Allow the ticks of Axis to be decimals or not. Default: True
             allow_data_overflow: When domain of the axis is specified and the type of the axis is 'number', if allowDataOverflow is set to be false, the domain will be adjusted when the minimum value of data is smaller than domain[0] or the maximum value of data is greater than domain[1] so that the axis displays all data values. If set to true, graphic elements (line, area, bars) will be clipped to conform to the specified domain. Default: False
             allow_duplicated_category: Allow the axis has duplicated categorys or not when the type of axis is "category". Default: True
-            domain: The range of the axis. Work best in conjuction with allow_data_overflow. Default: [0, "auto"]
+            domain: The range of the axis. Work best in conjunction with allow_data_overflow. Default: [0, "auto"]
             axis_line: If set false, no axis line will be drawn. Default: True
             mirror: If set true, flips ticks around the axis line, displaying the labels inside the chart instead of outside. Default: False
             reversed: Reverse the ticks or not. Default: False
@@ -512,13 +512,13 @@ class YAxis(Axis):
             data_key: The key of data displayed in the axis.
             hide: If set true, the axis do not display in the chart. Default: False
             width: The width of axis which is usually calculated internally.
-            height: The height of axis, which can be setted by user.
+            height: The height of axis, which can be set by user.
             type_: The type of axis 'number' | 'category'
             interval: If set 0, all the ticks will be shown. If set preserveStart", "preserveEnd" or "preserveStartEnd", the ticks which is to be shown or hidden will be calculated automatically. Default: "preserveEnd"
             allow_decimals: Allow the ticks of Axis to be decimals or not. Default: True
             allow_data_overflow: When domain of the axis is specified and the type of the axis is 'number', if allowDataOverflow is set to be false, the domain will be adjusted when the minimum value of data is smaller than domain[0] or the maximum value of data is greater than domain[1] so that the axis displays all data values. If set to true, graphic elements (line, area, bars) will be clipped to conform to the specified domain. Default: False
             allow_duplicated_category: Allow the axis has duplicated categorys or not when the type of axis is "category". Default: True
-            domain: The range of the axis. Work best in conjuction with allow_data_overflow. Default: [0, "auto"]
+            domain: The range of the axis. Work best in conjunction with allow_data_overflow. Default: [0, "auto"]
             axis_line: If set false, no axis line will be drawn. Default: True
             mirror: If set true, flips ticks around the axis line, displaying the labels inside the chart instead of outside. Default: False
             reversed: Reverse the ticks or not. Default: False

+ 4 - 4
reflex/components/recharts/charts.py

@@ -84,10 +84,10 @@ class ChartBase(RechartsCharts):
         cls._ensure_valid_dimension("width", width)
         cls._ensure_valid_dimension("height", height)
 
-        dim_props = dict(
-            width=width or "100%",
-            height=height or "100%",
-        )
+        dim_props = {
+            "width": width or "100%",
+            "height": height or "100%",
+        }
         # Provide min dimensions so the graph always appears, even if the outer container is zero-size.
         if width is None:
             dim_props["min_width"] = 200

+ 2 - 2
reflex/components/recharts/polar.py

@@ -124,7 +124,7 @@ class Radar(Recharts):
     # The key of a group of data which should be unique in a radar chart.
     data_key: Var[Union[str, int]]
 
-    # The coordinates of all the vertexes of the radar shape, like [{ x, y }].
+    # The coordinates of all the vertices of the radar shape, like [{ x, y }].
     points: Var[List[Dict[str, Any]]]
 
     # If false set, dots will not be drawn. Default: True
@@ -373,7 +373,7 @@ class PolarRadiusAxis(Recharts):
     # The count of axis ticks. Not used if 'type' is 'category'. Default: 5
     tick_count: Var[int]
 
-    # If 'auto' set, the scale funtion is linear scale. 'auto' | 'linear' | 'pow' | 'sqrt' | 'log' | 'identity' | 'time' | 'band' | 'point' | 'ordinal' | 'quantile' | 'quantize' | 'utc' | 'sequential' | 'threshold'. Default: "auto"
+    # If 'auto' set, the scale function is linear scale. 'auto' | 'linear' | 'pow' | 'sqrt' | 'log' | 'identity' | 'time' | 'band' | 'point' | 'ordinal' | 'quantile' | 'quantize' | 'utc' | 'sequential' | 'threshold'. Default: "auto"
     scale: Var[LiteralScale]
 
     # Valid children components

+ 2 - 2
reflex/components/recharts/polar.pyi

@@ -200,7 +200,7 @@ class Radar(Recharts):
         Args:
             *children: The children of the component.
             data_key: The key of a group of data which should be unique in a radar chart.
-            points: The coordinates of all the vertexes of the radar shape, like [{ x, y }].
+            points: The coordinates of all the vertices of the radar shape, like [{ x, y }].
             dot: If false set, dots will not be drawn. Default: True
             stroke: Stoke color. Default: rx.color("accent", 9)
             fill: Fill color. Default: rx.color("accent", 3)
@@ -574,7 +574,7 @@ class PolarRadiusAxis(Recharts):
             axis_line: If false set, axis line will not be drawn. If true set, axis line will be drawn which have the props calculated internally. If object set, axis line will be drawn which have the props mergered by the internal calculated props and the option. Default: True
             tick: If false set, ticks will not be drawn. If true set, ticks will be drawn which have the props calculated internally. If object set, ticks will be drawn which have the props mergered by the internal calculated props and the option. Default: True
             tick_count: The count of axis ticks. Not used if 'type' is 'category'. Default: 5
-            scale: If 'auto' set, the scale funtion is linear scale. 'auto' | 'linear' | 'pow' | 'sqrt' | 'log' | 'identity' | 'time' | 'band' | 'point' | 'ordinal' | 'quantile' | 'quantize' | 'utc' | 'sequential' | 'threshold'. Default: "auto"
+            scale: If 'auto' set, the scale function is linear scale. 'auto' | 'linear' | 'pow' | 'sqrt' | 'log' | 'identity' | 'time' | 'band' | 'point' | 'ordinal' | 'quantile' | 'quantize' | 'utc' | 'sequential' | 'threshold'. Default: "auto"
             domain: The domain of the polar radius axis, specifying the minimum and maximum values. Default: [0, "auto"]
             stroke: The stroke color of axis. Default: rx.color("gray", 10)
             style: The style of the component.

+ 7 - 4
reflex/config.py

@@ -82,7 +82,7 @@ class DBConfig(Base):
         )
 
     @classmethod
-    def postgresql_psycopg2(
+    def postgresql_psycopg(
         cls,
         database: str,
         username: str,
@@ -90,7 +90,7 @@ class DBConfig(Base):
         host: str | None = None,
         port: int | None = 5432,
     ) -> DBConfig:
-        """Create an instance with postgresql+psycopg2 engine.
+        """Create an instance with postgresql+psycopg engine.
 
         Args:
             database: Database name.
@@ -103,7 +103,7 @@ class DBConfig(Base):
             DBConfig instance.
         """
         return cls(
-            engine="postgresql+psycopg2",
+            engine="postgresql+psycopg",
             username=username,
             password=password,
             host=host,
@@ -684,6 +684,9 @@ class Config(Base):
     # Maximum expiration lock time for redis state manager
     redis_lock_expiration: int = constants.Expiration.LOCK
 
+    # Maximum lock time before warning for redis state manager.
+    redis_lock_warning_threshold: int = constants.Expiration.LOCK_WARNING_THRESHOLD
+
     # Token expiration time for redis state manager
     redis_token_expiration: int = constants.Expiration.TOKEN
 
@@ -870,7 +873,7 @@ def get_config(reload: bool = False) -> Config:
     with _config_lock:
         sys_path = sys.path.copy()
         sys.path.clear()
-        sys.path.append(os.getcwd())
+        sys.path.append(str(Path.cwd()))
         try:
             # Try to import the module with only the current directory in the path.
             return _get_config()

+ 1 - 1
reflex/constants/base.py

@@ -27,7 +27,7 @@ class Dirs(SimpleNamespace):
     UPLOADED_FILES = "uploaded_files"
     # The name of the assets directory.
     APP_ASSETS = "assets"
-    # The name of the assets directory for external ressource (a subfolder of APP_ASSETS).
+    # The name of the assets directory for external resources (a subfolder of APP_ASSETS).
     EXTERNAL_APP_ASSETS = "external"
     # The name of the utils file.
     UTILS = "utils"

+ 6 - 0
reflex/constants/compiler.py

@@ -132,6 +132,12 @@ class Hooks(SimpleNamespace):
                   }
                 })"""
 
+    class HookPosition(enum.Enum):
+        """The position of the hook in the component."""
+
+        PRE_TRIGGER = "pre_trigger"
+        POST_TRIGGER = "post_trigger"
+
 
 class MemoizationDisposition(enum.Enum):
     """The conditions under which a component should be memoized."""

+ 2 - 0
reflex/constants/config.py

@@ -29,6 +29,8 @@ class Expiration(SimpleNamespace):
     LOCK = 10000
     # The PING timeout
     PING = 120
+    # The maximum time in milliseconds to hold a lock before throwing a warning.
+    LOCK_WARNING_THRESHOLD = 1000
 
 
 class GitIgnore(SimpleNamespace):

+ 1 - 1
reflex/constants/custom_components.py

@@ -10,7 +10,7 @@ class CustomComponents(SimpleNamespace):
     """Constants for the custom components."""
 
     # The name of the custom components source directory.
-    SRC_DIR = "custom_components"
+    SRC_DIR = Path("custom_components")
     # The name of the custom components pyproject.toml file.
     PYPROJECT_TOML = Path("pyproject.toml")
     # The name of the custom components package README file.

+ 1 - 1
reflex/constants/route.py

@@ -31,7 +31,7 @@ class RouteVar(SimpleNamespace):
 
 
 # This subset of router_data is included in chained on_load events.
-ROUTER_DATA_INCLUDE = set((RouteVar.PATH, RouteVar.ORIGIN, RouteVar.QUERY))
+ROUTER_DATA_INCLUDE = {RouteVar.PATH, RouteVar.ORIGIN, RouteVar.QUERY}
 
 
 class RouteRegex(SimpleNamespace):

+ 21 - 21
reflex/custom_components/custom_components.py

@@ -150,27 +150,27 @@ def _populate_demo_app(name_variants: NameVariants):
     from reflex.compiler import templates
     from reflex.reflex import _init
 
-    demo_app_dir = name_variants.demo_app_dir
+    demo_app_dir = Path(name_variants.demo_app_dir)
     demo_app_name = name_variants.demo_app_name
 
-    console.info(f"Creating app for testing: {demo_app_dir}")
+    console.info(f"Creating app for testing: {demo_app_dir!s}")
 
-    os.makedirs(demo_app_dir)
+    demo_app_dir.mkdir(exist_ok=True)
 
     with set_directory(demo_app_dir):
         # We start with the blank template as basis.
         _init(name=demo_app_name, template=constants.Templates.DEFAULT)
         # Then overwrite the app source file with the one we want for testing custom components.
         # This source file is rendered using jinja template file.
-        with open(f"{demo_app_name}/{demo_app_name}.py", "w") as f:
-            f.write(
-                templates.CUSTOM_COMPONENTS_DEMO_APP.render(
-                    custom_component_module_dir=name_variants.custom_component_module_dir,
-                    module_name=name_variants.module_name,
-                )
+        demo_file = Path(f"{demo_app_name}/{demo_app_name}.py")
+        demo_file.write_text(
+            templates.CUSTOM_COMPONENTS_DEMO_APP.render(
+                custom_component_module_dir=name_variants.custom_component_module_dir,
+                module_name=name_variants.module_name,
             )
+        )
         # Append the custom component package to the requirements.txt file.
-        with open(f"{constants.RequirementsTxt.FILE}", "a") as f:
+        with Path(f"{constants.RequirementsTxt.FILE}").open(mode="a") as f:
             f.write(f"{name_variants.package_name}\n")
 
 
@@ -296,13 +296,14 @@ def _populate_custom_component_project(name_variants: NameVariants):
     )
 
     console.info(
-        f"Initializing the component directory: {CustomComponents.SRC_DIR}/{name_variants.custom_component_module_dir}"
+        f"Initializing the component directory: {CustomComponents.SRC_DIR / name_variants.custom_component_module_dir}"
     )
-    os.makedirs(CustomComponents.SRC_DIR)
+    CustomComponents.SRC_DIR.mkdir(exist_ok=True)
     with set_directory(CustomComponents.SRC_DIR):
-        os.makedirs(name_variants.custom_component_module_dir)
+        module_dir = Path(name_variants.custom_component_module_dir)
+        module_dir.mkdir(exist_ok=True, parents=True)
         _write_source_and_init_py(
-            custom_component_src_dir=name_variants.custom_component_module_dir,
+            custom_component_src_dir=module_dir,
             component_class_name=name_variants.component_class_name,
             module_name=name_variants.module_name,
         )
@@ -814,7 +815,7 @@ def _validate_project_info():
     )
     pyproject_toml["project"] = project
     try:
-        with open(CustomComponents.PYPROJECT_TOML, "w") as f:
+        with CustomComponents.PYPROJECT_TOML.open("w") as f:
             tomlkit.dump(pyproject_toml, f)
     except (OSError, TOMLKitError) as ex:
         console.error(f"Unable to write to pyproject.toml due to {ex}")
@@ -922,16 +923,15 @@ def _validate_url_with_protocol_prefix(url: str | None) -> bool:
 def _get_file_from_prompt_in_loop() -> Tuple[bytes, str] | None:
     image_file = file_extension = None
     while image_file is None:
-        image_filepath = console.ask(
-            "Upload a preview image of your demo app (enter to skip)"
+        image_filepath = Path(
+            console.ask("Upload a preview image of your demo app (enter to skip)")
         )
         if not image_filepath:
             break
-        file_extension = image_filepath.split(".")[-1]
+        file_extension = image_filepath.suffix
         try:
-            with open(image_filepath, "rb") as f:
-                image_file = f.read()
-                return image_file, file_extension
+            image_file = image_filepath.read_bytes()
+            return image_file, file_extension
         except OSError as ose:
             console.error(f"Unable to read the {file_extension} file due to {ose}")
             raise typer.Exit(code=1) from ose

+ 53 - 18
reflex/event.py

@@ -25,6 +25,7 @@ from typing import (
     overload,
 )
 
+import typing_extensions
 from typing_extensions import (
     Concatenate,
     ParamSpec,
@@ -296,7 +297,7 @@ class EventSpec(EventActionsMixin):
         handler: EventHandler,
         event_actions: Dict[str, Union[bool, int]] | None = None,
         client_handler_name: str = "",
-        args: Tuple[Tuple[Var, Var], ...] = tuple(),
+        args: Tuple[Tuple[Var, Var], ...] = (),
     ):
         """Initialize an EventSpec.
 
@@ -311,7 +312,7 @@ class EventSpec(EventActionsMixin):
         object.__setattr__(self, "event_actions", event_actions)
         object.__setattr__(self, "handler", handler)
         object.__setattr__(self, "client_handler_name", client_handler_name)
-        object.__setattr__(self, "args", args or tuple())
+        object.__setattr__(self, "args", args or ())
 
     def with_args(self, args: Tuple[Tuple[Var, Var], ...]) -> EventSpec:
         """Copy the event spec, with updated args.
@@ -349,13 +350,14 @@ class EventSpec(EventActionsMixin):
 
         # Construct the payload.
         values = []
-        for arg in args:
-            try:
-                values.append(LiteralVar.create(arg))
-            except TypeError as e:
-                raise EventHandlerTypeError(
-                    f"Arguments to event handlers must be Vars or JSON-serializable. Got {arg} of type {type(arg)}."
-                ) from e
+        arg = None
+        try:
+            for arg in args:
+                values.append(LiteralVar.create(value=arg))  # noqa: PERF401
+        except TypeError as e:
+            raise EventHandlerTypeError(
+                f"Arguments to event handlers must be Vars or JSON-serializable. Got {arg} of type {type(arg)}."
+            ) from e
         new_payload = tuple(zip(fn_args, values))
         return self.with_args(self.args + new_payload)
 
@@ -513,7 +515,7 @@ def no_args_event_spec() -> Tuple[()]:
     Returns:
         An empty tuple.
     """
-    return tuple()  # type: ignore
+    return ()  # type: ignore
 
 
 # These chains can be used for their side effects when no other events are desired.
@@ -714,26 +716,61 @@ def server_side(name: str, sig: inspect.Signature, **kwargs) -> EventSpec:
     )
 
 
+@overload
+def redirect(
+    path: str | Var[str],
+    is_external: Optional[bool] = None,
+    replace: bool = False,
+) -> EventSpec: ...
+
+
+@overload
+@typing_extensions.deprecated("`external` is deprecated use `is_external` instead")
+def redirect(
+    path: str | Var[str],
+    is_external: Optional[bool] = None,
+    replace: bool = False,
+    external: Optional[bool] = None,
+) -> EventSpec: ...
+
+
 def redirect(
     path: str | Var[str],
-    external: Optional[bool] = False,
-    replace: Optional[bool] = False,
+    is_external: Optional[bool] = None,
+    replace: bool = False,
+    external: Optional[bool] = None,
 ) -> EventSpec:
     """Redirect to a new path.
 
     Args:
         path: The path to redirect to.
-        external: Whether to open in new tab or not.
+        is_external: Whether to open in new tab or not.
         replace: If True, the current page will not create a new history entry.
+        external(Deprecated): Whether to open in new tab or not.
 
     Returns:
         An event to redirect to the path.
     """
+    if external is not None:
+        console.deprecate(
+            "The `external` prop in `rx.redirect`",
+            "use `is_external` instead.",
+            "0.6.6",
+            "0.7.0",
+        )
+
+    # is_external should take precedence over external.
+    is_external = (
+        (False if external is None else external)
+        if is_external is None
+        else is_external
+    )
+
     return server_side(
         "_redirect",
         get_fn_signature(redirect),
         path=path,
-        external=external,
+        external=is_external,
         replace=replace,
     )
 
@@ -1101,9 +1138,7 @@ def run_script(
         Var(javascript_code) if isinstance(javascript_code, str) else javascript_code
     )
 
-    return call_function(
-        ArgsFunctionOperation.create(tuple(), javascript_code), callback
-    )
+    return call_function(ArgsFunctionOperation.create((), javascript_code), callback)
 
 
 def get_event(state, event):
@@ -1455,7 +1490,7 @@ def get_handler_args(
     """
     args = inspect.getfullargspec(event_spec.handler.fn).args
 
-    return event_spec.args if len(args) > 1 else tuple()
+    return event_spec.args if len(args) > 1 else ()
 
 
 def fix_events(

+ 3 - 3
reflex/istate/data.py

@@ -26,7 +26,7 @@ class HeaderData:
     accept_language: str = ""
 
     def __init__(self, router_data: Optional[dict] = None):
-        """Initalize the HeaderData object based on router_data.
+        """Initialize the HeaderData object based on router_data.
 
         Args:
             router_data: the router_data dict.
@@ -51,7 +51,7 @@ class PageData:
     params: dict = dataclasses.field(default_factory=dict)
 
     def __init__(self, router_data: Optional[dict] = None):
-        """Initalize the PageData object based on router_data.
+        """Initialize the PageData object based on router_data.
 
         Args:
             router_data: the router_data dict.
@@ -91,7 +91,7 @@ class SessionData:
     session_id: str = ""
 
     def __init__(self, router_data: Optional[dict] = None):
-        """Initalize the SessionData object based on router_data.
+        """Initialize the SessionData object based on router_data.
 
         Args:
             router_data: the router_data dict.

+ 11 - 13
reflex/model.py

@@ -4,6 +4,7 @@ from __future__ import annotations
 
 import re
 from collections import defaultdict
+from contextlib import suppress
 from typing import Any, ClassVar, Optional, Type, Union
 
 import alembic.autogenerate
@@ -52,12 +53,12 @@ def get_engine_args(url: str | None = None) -> dict[str, Any]:
     Returns:
         The database engine arguments as a dict.
     """
-    kwargs: dict[str, Any] = dict(
+    kwargs: dict[str, Any] = {
         # Print the SQL queries if the log level is INFO or lower.
-        echo=environment.SQLALCHEMY_ECHO.get(),
+        "echo": environment.SQLALCHEMY_ECHO.get(),
         # Check connections before returning them.
-        pool_pre_ping=environment.SQLALCHEMY_POOL_PRE_PING.get(),
-    )
+        "pool_pre_ping": environment.SQLALCHEMY_POOL_PRE_PING.get(),
+    }
     conf = get_config()
     url = url or conf.db_url
     if url is not None and url.startswith("sqlite"):
@@ -140,15 +141,13 @@ def get_async_engine(url: str | None) -> sqlalchemy.ext.asyncio.AsyncEngine:
     return _ASYNC_ENGINE[url]
 
 
-async def get_db_status() -> bool:
+async def get_db_status() -> dict[str, bool]:
     """Checks the status of the database connection.
 
     Attempts to connect to the database and execute a simple query to verify connectivity.
 
     Returns:
-        bool: The status of the database connection:
-            - True: The database is accessible.
-            - False: The database is not accessible.
+        The status of the database connection.
     """
     status = True
     try:
@@ -158,7 +157,7 @@ async def get_db_status() -> bool:
     except sqlalchemy.exc.OperationalError:
         status = False
 
-    return status
+    return {"db": status}
 
 
 SQLModelOrSqlAlchemy = Union[
@@ -290,11 +289,10 @@ class Model(Base, sqlmodel.SQLModel):  # pyright: ignore [reportGeneralTypeIssue
         relationships = {}
         # SQLModel relationships do not appear in __fields__, but should be included if present.
         for name in self.__sqlmodel_relationships__:
-            try:
+            with suppress(
+                sqlalchemy.orm.exc.DetachedInstanceError  # This happens when the relationship was never loaded and the session is closed.
+            ):
                 relationships[name] = self._dict_recursive(getattr(self, name))
-            except sqlalchemy.orm.exc.DetachedInstanceError:
-                # This happens when the relationship was never loaded and the session is closed.
-                continue
         return {
             **base_fields,
             **relationships,

+ 1 - 1
reflex/page.py

@@ -70,7 +70,7 @@ def get_decorated_pages(omit_implicit_routes=True) -> list[dict[str, Any]]:
     """Get the decorated pages.
 
     Args:
-        omit_implicit_routes: Whether to omit pages where the route will be implicitely guessed later.
+        omit_implicit_routes: Whether to omit pages where the route will be implicitly guessed later.
 
     Returns:
         The decorated pages.

+ 5 - 5
reflex/reflex.py

@@ -3,7 +3,6 @@
 from __future__ import annotations
 
 import atexit
-import os
 from pathlib import Path
 from typing import List, Optional
 
@@ -298,7 +297,7 @@ def export(
         True, "--frontend-only", help="Export only frontend.", show_default=False
     ),
     zip_dest_dir: str = typer.Option(
-        os.getcwd(),
+        str(Path.cwd()),
         help="The directory to export the zip files to.",
         show_default=False,
     ),
@@ -330,13 +329,14 @@ def export(
 
 @cli.command()
 def login(loglevel: constants.LogLevel = typer.Option(config.loglevel)):
-    """Authenicate with experimental Reflex hosting service."""
+    """Authenticate with experimental Reflex hosting service."""
     from reflex_cli.v2 import cli as hosting_cli
 
     check_version()
 
     validated_info = hosting_cli.login()
     if validated_info is not None:
+        _skip_compile()  # Allow running outside of an app dir
         telemetry.send("login", user_uuid=validated_info.get("user_id"))
 
 
@@ -443,13 +443,13 @@ def deploy(
         hidden=True,
     ),
     regions: List[str] = typer.Option(
-        list(),
+        [],
         "-r",
         "--region",
         help="The regions to deploy to. `reflex cloud regions` For multiple envs, repeat this option, e.g. --region sjc --region iad",
     ),
     envs: List[str] = typer.Option(
-        list(),
+        [],
         "--env",
         help="The environment variables to set: <key>=<value>. For multiple envs, repeat this option, e.g. --env k1=v2 --env k2=v2.",
     ),

+ 162 - 82
reflex/state.py

@@ -11,6 +11,7 @@ import inspect
 import json
 import pickle
 import sys
+import time
 import typing
 import uuid
 from abc import ABC, abstractmethod
@@ -39,6 +40,7 @@ from typing import (
     get_type_hints,
 )
 
+from redis.asyncio.client import PubSub
 from sqlalchemy.orm import DeclarativeBase
 from typing_extensions import Self
 
@@ -69,6 +71,11 @@ try:
 except ModuleNotFoundError:
     BaseModelV1 = BaseModelV2
 
+try:
+    from pydantic.v1 import validator
+except ModuleNotFoundError:
+    from pydantic import validator
+
 import wrapt
 from redis.asyncio import Redis
 from redis.exceptions import ResponseError
@@ -92,6 +99,7 @@ from reflex.utils.exceptions import (
     DynamicRouteArgShadowsStateVar,
     EventHandlerShadowsBuiltInStateMethod,
     ImmutableStateError,
+    InvalidLockWarningThresholdError,
     InvalidStateManagerMode,
     LockExpiredError,
     ReflexRuntimeError,
@@ -429,9 +437,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
                 )
 
         # Create a fresh copy of the backend variables for this instance
-        self._backend_vars = copy.deepcopy(
-            {name: item for name, item in self.backend_vars.items()}
-        )
+        self._backend_vars = copy.deepcopy(self.backend_vars)
 
     def __repr__(self) -> str:
         """Get the string representation of the state.
@@ -515,9 +521,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
             cls.inherited_backend_vars = parent_state.backend_vars
 
             # Check if another substate class with the same name has already been defined.
-            if cls.get_name() in set(
-                c.get_name() for c in parent_state.class_subclasses
-            ):
+            if cls.get_name() in {c.get_name() for c in parent_state.class_subclasses}:
                 # This should not happen, since we have added module prefix to state names in #3214
                 raise StateValueError(
                     f"The substate class '{cls.get_name()}' has been defined multiple times. "
@@ -780,11 +784,11 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
                         )
 
         # ComputedVar with cache=False always need to be recomputed
-        cls._always_dirty_computed_vars = set(
+        cls._always_dirty_computed_vars = {
             cvar_name
             for cvar_name, cvar in cls.computed_vars.items()
             if not cvar._cache
-        )
+        }
 
         # Any substate containing a ComputedVar with cache=False always needs to be recomputed
         if cls._always_dirty_computed_vars:
@@ -1095,6 +1099,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
         if (
             not field.required
             and field.default is None
+            and field.default_factory is None
             and not types.is_optional(prop._var_type)
         ):
             # Ensure frontend uses null coalescing when accessing.
@@ -1235,13 +1240,16 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
         if not super().__getattribute__("__dict__"):
             return super().__getattribute__(name)
 
-        inherited_vars = {
-            **super().__getattribute__("inherited_vars"),
-            **super().__getattribute__("inherited_backend_vars"),
-        }
+        # Fast path for dunder
+        if name.startswith("__"):
+            return super().__getattribute__(name)
 
         # For now, handle router_data updates as a special case.
-        if name in inherited_vars or name == constants.ROUTER_DATA:
+        if (
+            name == constants.ROUTER_DATA
+            or name in super().__getattribute__("inherited_vars")
+            or name in super().__getattribute__("inherited_backend_vars")
+        ):
             parent_state = super().__getattribute__("parent_state")
             if parent_state is not None:
                 return getattr(parent_state, name)
@@ -1296,15 +1304,11 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
             value = value.__wrapped__
 
         # Set the var on the parent state.
-        inherited_vars = {**self.inherited_vars, **self.inherited_backend_vars}
-        if name in inherited_vars:
+        if name in self.inherited_vars or name in self.inherited_backend_vars:
             setattr(self.parent_state, name, value)
             return
 
         if name in self.backend_vars:
-            # abort if unchanged
-            if self._backend_vars.get(name) == value:
-                return
             self._backend_vars.__setitem__(name, value)
             self.dirty_vars.add(name)
             self._mark_dirty()
@@ -1853,11 +1857,11 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
         Returns:
             Set of computed vars to include in the delta.
         """
-        return set(
+        return {
             cvar
             for cvar in self.computed_vars
             if self.computed_vars[cvar].needs_update(instance=self)
-        )
+        }
 
     def _dirty_computed_vars(
         self, from_vars: set[str] | None = None, include_backend: bool = True
@@ -1871,12 +1875,12 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
         Returns:
             Set of computed vars to include in the delta.
         """
-        return set(
+        return {
             cvar
             for dirty_var in from_vars or self.dirty_vars
             for cvar in self._computed_var_dependencies[dirty_var]
             if include_backend or not self.computed_vars[cvar]._backend
-        )
+        }
 
     @classmethod
     def _potentially_dirty_substates(cls) -> set[Type[BaseState]]:
@@ -1886,16 +1890,16 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
             Set of State classes that may need to be fetched to recalc computed vars.
         """
         # _always_dirty_substates need to be fetched to recalc computed vars.
-        fetch_substates = set(
+        fetch_substates = {
             cls.get_class_substate((cls.get_name(), *substate_name.split(".")))
             for substate_name in cls._always_dirty_substates
-        )
+        }
         for dependent_substates in cls._substate_var_dependencies.values():
             fetch_substates.update(
-                set(
+                {
                     cls.get_class_substate((cls.get_name(), *substate_name.split(".")))
                     for substate_name in dependent_substates
-                )
+                }
             )
         return fetch_substates
 
@@ -2122,14 +2126,26 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
             state["__dict__"].pop("router", None)
             state["__dict__"].pop("router_data", None)
         # Never serialize parent_state or substates.
-        state["__dict__"]["parent_state"] = None
-        state["__dict__"]["substates"] = {}
+        state["__dict__"].pop("parent_state", None)
+        state["__dict__"].pop("substates", None)
         state["__dict__"].pop("_was_touched", None)
         # Remove all inherited vars.
         for inherited_var_name in self.inherited_vars:
             state["__dict__"].pop(inherited_var_name, None)
         return state
 
+    def __setstate__(self, state: dict[str, Any]):
+        """Set the state from redis deserialization.
+
+        This method is called by pickle to deserialize the object.
+
+        Args:
+            state: The state dict for deserialization.
+        """
+        state["__dict__"]["parent_state"] = None
+        state["__dict__"]["substates"] = {}
+        super().__setstate__(state)
+
     def _check_state_size(
         self,
         pickle_state_size: int,
@@ -2185,7 +2201,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
 
         return md5(
             pickle.dumps(
-                list(sorted(_field_tuple(field_name) for field_name in cls.base_vars))
+                sorted(_field_tuple(field_name) for field_name in cls.base_vars)
             )
         ).hexdigest()
 
@@ -2819,6 +2835,7 @@ class StateManager(Base, ABC):
                     redis=redis,
                     token_expiration=config.redis_token_expiration,
                     lock_expiration=config.redis_lock_expiration,
+                    lock_warning_threshold=config.redis_lock_warning_threshold,
                 )
         raise InvalidStateManagerMode(
             f"Expected one of: DISK, MEMORY, REDIS, got {config.state_manager_mode}"
@@ -3188,6 +3205,15 @@ def _default_lock_expiration() -> int:
     return get_config().redis_lock_expiration
 
 
+def _default_lock_warning_threshold() -> int:
+    """Get the default lock warning threshold.
+
+    Returns:
+        The default lock warning threshold.
+    """
+    return get_config().redis_lock_warning_threshold
+
+
 class StateManagerRedis(StateManager):
     """A state manager that stores states in redis."""
 
@@ -3200,6 +3226,11 @@ class StateManagerRedis(StateManager):
     # The maximum time to hold a lock (ms).
     lock_expiration: int = pydantic.Field(default_factory=_default_lock_expiration)
 
+    # The maximum time to hold a lock (ms) before warning.
+    lock_warning_threshold: int = pydantic.Field(
+        default_factory=_default_lock_warning_threshold
+    )
+
     # The keyspace subscription string when redis is waiting for lock to be released
     _redis_notify_keyspace_events: str = (
         "K"  # Enable keyspace notifications (target a particular key)
@@ -3318,7 +3349,7 @@ class StateManagerRedis(StateManager):
             state_cls = self.state.get_class_substate(state_path)
         else:
             raise RuntimeError(
-                "StateManagerRedis requires token to be specified in the form of {token}_{state_full_name}"
+                f"StateManagerRedis requires token to be specified in the form of {{token}}_{{state_full_name}}, but got {token}"
             )
 
         # The deserialized or newly created (sub)state instance.
@@ -3387,6 +3418,17 @@ class StateManagerRedis(StateManager):
                 f"`app.state_manager.lock_expiration` (currently {self.lock_expiration}) "
                 "or use `@rx.event(background=True)` decorator for long-running tasks."
             )
+        elif lock_id is not None:
+            time_taken = self.lock_expiration / 1000 - (
+                await self.redis.ttl(self._lock_key(token))
+            )
+            if time_taken > self.lock_warning_threshold / 1000:
+                console.warn(
+                    f"Lock for token {token} was held too long {time_taken=}s, "
+                    f"use `@rx.event(background=True)` decorator for long-running tasks.",
+                    dedupe=True,
+                )
+
         client_token, substate_name = _split_substate_key(token)
         # If the substate name on the token doesn't match the instance name, it cannot have a parent.
         if state.parent_state is not None and state.get_full_name() != substate_name:
@@ -3395,17 +3437,16 @@ class StateManagerRedis(StateManager):
             )
 
         # Recursively set_state on all known substates.
-        tasks = []
-        for substate in state.substates.values():
-            tasks.append(
-                asyncio.create_task(
-                    self.set_state(
-                        token=_substate_key(client_token, substate),
-                        state=substate,
-                        lock_id=lock_id,
-                    )
+        tasks = [
+            asyncio.create_task(
+                self.set_state(
+                    _substate_key(client_token, substate),
+                    substate,
+                    lock_id,
                 )
             )
+            for substate in state.substates.values()
+        ]
         # Persist only the given state (parents or substates are excluded by BaseState.__getstate__).
         if state._get_was_touched():
             pickle_state = state._serialize()
@@ -3436,6 +3477,27 @@ class StateManagerRedis(StateManager):
             yield state
             await self.set_state(token, state, lock_id)
 
+    @validator("lock_warning_threshold")
+    @classmethod
+    def validate_lock_warning_threshold(cls, lock_warning_threshold: int, values):
+        """Validate the lock warning threshold.
+
+        Args:
+            lock_warning_threshold: The lock warning threshold.
+            values: The validated attributes.
+
+        Returns:
+            The lock warning threshold.
+
+        Raises:
+            InvalidLockWarningThresholdError: If the lock warning threshold is invalid.
+        """
+        if lock_warning_threshold >= (lock_expiration := values["lock_expiration"]):
+            raise InvalidLockWarningThresholdError(
+                f"The lock warning threshold({lock_warning_threshold}) must be less than the lock expiration time({lock_expiration})."
+            )
+        return lock_warning_threshold
+
     @staticmethod
     def _lock_key(token: str) -> bytes:
         """Get the redis key for a token's lock.
@@ -3467,6 +3529,35 @@ class StateManagerRedis(StateManager):
             nx=True,  # only set if it doesn't exist
         )
 
+    async def _get_pubsub_message(
+        self, pubsub: PubSub, timeout: float | None = None
+    ) -> None:
+        """Get lock release events from the pubsub.
+
+        Args:
+            pubsub: The pubsub to get a message from.
+            timeout: Remaining time to wait for a message.
+
+        Returns:
+            The message.
+        """
+        if timeout is None:
+            timeout = self.lock_expiration / 1000.0
+
+        started = time.time()
+        message = await pubsub.get_message(
+            ignore_subscribe_messages=True,
+            timeout=timeout,
+        )
+        if (
+            message is None
+            or message["data"] not in self._redis_keyspace_lock_release_events
+        ):
+            remaining = timeout - (time.time() - started)
+            if remaining <= 0:
+                return
+            await self._get_pubsub_message(pubsub, timeout=remaining)
+
     async def _wait_lock(self, lock_key: bytes, lock_id: bytes) -> None:
         """Wait for a redis lock to be released via pubsub.
 
@@ -3479,7 +3570,6 @@ class StateManagerRedis(StateManager):
         Raises:
             ResponseError: when the keyspace config cannot be set.
         """
-        state_is_locked = False
         lock_key_channel = f"__keyspace@0__:{lock_key.decode()}"
         # Enable keyspace notifications for the lock key, so we know when it is available.
         try:
@@ -3493,20 +3583,13 @@ class StateManagerRedis(StateManager):
                 raise
         async with self.redis.pubsub() as pubsub:
             await pubsub.psubscribe(lock_key_channel)
-            while not state_is_locked:
-                # wait for the lock to be released
-                while True:
-                    if not await self.redis.exists(lock_key):
-                        break  # key was removed, try to get the lock again
-                    message = await pubsub.get_message(
-                        ignore_subscribe_messages=True,
-                        timeout=self.lock_expiration / 1000.0,
-                    )
-                    if message is None:
-                        continue
-                    if message["data"] in self._redis_keyspace_lock_release_events:
-                        break
-                state_is_locked = await self._try_get_lock(lock_key, lock_id)
+            # wait for the lock to be released
+            while True:
+                # fast path
+                if await self._try_get_lock(lock_key, lock_id):
+                    return
+                # wait for lock events
+                await self._get_pubsub_message(pubsub)
 
     @contextlib.asynccontextmanager
     async def _lock(self, token: str):
@@ -3565,33 +3648,30 @@ class MutableProxy(wrapt.ObjectProxy):
     """A proxy for a mutable object that tracks changes."""
 
     # Methods on wrapped objects which should mark the state as dirty.
-    __mark_dirty_attrs__ = set(
-        [
-            "add",
-            "append",
-            "clear",
-            "difference_update",
-            "discard",
-            "extend",
-            "insert",
-            "intersection_update",
-            "pop",
-            "popitem",
-            "remove",
-            "reverse",
-            "setdefault",
-            "sort",
-            "symmetric_difference_update",
-            "update",
-        ]
-    )
+    __mark_dirty_attrs__ = {
+        "add",
+        "append",
+        "clear",
+        "difference_update",
+        "discard",
+        "extend",
+        "insert",
+        "intersection_update",
+        "pop",
+        "popitem",
+        "remove",
+        "reverse",
+        "setdefault",
+        "sort",
+        "symmetric_difference_update",
+        "update",
+    }
+
     # Methods on wrapped objects might return mutable objects that should be tracked.
-    __wrap_mutable_attrs__ = set(
-        [
-            "get",
-            "setdefault",
-        ]
-    )
+    __wrap_mutable_attrs__ = {
+        "get",
+        "setdefault",
+    }
 
     # These internal attributes on rx.Base should NOT be wrapped in a MutableProxy.
     __never_wrap_base_attrs__ = set(Base.__dict__) - {"set"} | set(
@@ -3634,7 +3714,7 @@ class MutableProxy(wrapt.ObjectProxy):
         self,
         wrapped=None,
         instance=None,
-        args=tuple(),
+        args=(),
         kwargs=None,
     ) -> Any:
         """Mark the state as dirty, then call a wrapped function.
@@ -3890,7 +3970,7 @@ class ImmutableMutableProxy(MutableProxy):
         self,
         wrapped=None,
         instance=None,
-        args=tuple(),
+        args=(),
         kwargs=None,
     ) -> Any:
         """Raise an exception when an attempt is made to modify the object.

+ 8 - 8
reflex/testing.py

@@ -8,7 +8,6 @@ import dataclasses
 import functools
 import inspect
 import os
-import pathlib
 import platform
 import re
 import signal
@@ -20,6 +19,7 @@ import threading
 import time
 import types
 from http.server import SimpleHTTPRequestHandler
+from pathlib import Path
 from typing import (
     TYPE_CHECKING,
     Any,
@@ -100,7 +100,7 @@ class chdir(contextlib.AbstractContextManager):
 
     def __enter__(self):
         """Save current directory and perform chdir."""
-        self._old_cwd.append(os.getcwd())
+        self._old_cwd.append(Path.cwd())
         os.chdir(self.path)
 
     def __exit__(self, *excinfo):
@@ -120,8 +120,8 @@ class AppHarness:
     app_source: Optional[
         Callable[[], None] | types.ModuleType | str | functools.partial[Any]
     ]
-    app_path: pathlib.Path
-    app_module_path: pathlib.Path
+    app_path: Path
+    app_module_path: Path
     app_module: Optional[types.ModuleType] = None
     app_instance: Optional[reflex.App] = None
     frontend_process: Optional[subprocess.Popen] = None
@@ -136,7 +136,7 @@ class AppHarness:
     @classmethod
     def create(
         cls,
-        root: pathlib.Path,
+        root: Path,
         app_source: Optional[
             Callable[[], None] | types.ModuleType | str | functools.partial[Any]
         ] = None,
@@ -814,7 +814,7 @@ class AppHarness:
 class SimpleHTTPRequestHandlerCustomErrors(SimpleHTTPRequestHandler):
     """SimpleHTTPRequestHandler with custom error page handling."""
 
-    def __init__(self, *args, error_page_map: dict[int, pathlib.Path], **kwargs):
+    def __init__(self, *args, error_page_map: dict[int, Path], **kwargs):
         """Initialize the handler.
 
         Args:
@@ -857,8 +857,8 @@ class Subdir404TCPServer(socketserver.TCPServer):
     def __init__(
         self,
         *args,
-        root: pathlib.Path,
-        error_page_map: dict[int, pathlib.Path] | None,
+        root: Path,
+        error_page_map: dict[int, Path] | None,
         **kwargs,
     ):
         """Initialize the server.

+ 1 - 1
reflex/utils/build.py

@@ -150,7 +150,7 @@ def zip_app(
         _zip(
             component_name=constants.ComponentName.BACKEND,
             target=zip_dest_dir / constants.ComponentName.BACKEND.zip(),
-            root_dir=Path("."),
+            root_dir=Path.cwd(),
             dirs_to_exclude={"__pycache__"},
             files_to_exclude=files_to_exclude,
             top_level_dirs_to_exclude={"assets"},

+ 60 - 6
reflex/utils/console.py

@@ -20,6 +20,24 @@ _EMITTED_DEPRECATION_WARNINGS = set()
 # Info messages which have been printed.
 _EMITTED_INFO = set()
 
+# Warnings which have been printed.
+_EMIITED_WARNINGS = set()
+
+# Errors which have been printed.
+_EMITTED_ERRORS = set()
+
+# Success messages which have been printed.
+_EMITTED_SUCCESS = set()
+
+# Debug messages which have been printed.
+_EMITTED_DEBUG = set()
+
+# Logs which have been printed.
+_EMITTED_LOGS = set()
+
+# Prints which have been printed.
+_EMITTED_PRINTS = set()
+
 
 def set_log_level(log_level: LogLevel):
     """Set the log level.
@@ -55,25 +73,37 @@ def is_debug() -> bool:
     return _LOG_LEVEL <= LogLevel.DEBUG
 
 
-def print(msg: str, **kwargs):
+def print(msg: str, dedupe: bool = False, **kwargs):
     """Print a message.
 
     Args:
         msg: The message to print.
+        dedupe: If True, suppress multiple console logs of print message.
         kwargs: Keyword arguments to pass to the print function.
     """
+    if dedupe:
+        if msg in _EMITTED_PRINTS:
+            return
+        else:
+            _EMITTED_PRINTS.add(msg)
     _console.print(msg, **kwargs)
 
 
-def debug(msg: str, **kwargs):
+def debug(msg: str, dedupe: bool = False, **kwargs):
     """Print a debug message.
 
     Args:
         msg: The debug message.
+        dedupe: If True, suppress multiple console logs of debug message.
         kwargs: Keyword arguments to pass to the print function.
     """
     if is_debug():
         msg_ = f"[purple]Debug: {msg}[/purple]"
+        if dedupe:
+            if msg_ in _EMITTED_DEBUG:
+                return
+            else:
+                _EMITTED_DEBUG.add(msg_)
         if progress := kwargs.pop("progress", None):
             progress.console.print(msg_, **kwargs)
         else:
@@ -97,25 +127,37 @@ def info(msg: str, dedupe: bool = False, **kwargs):
         print(f"[cyan]Info: {msg}[/cyan]", **kwargs)
 
 
-def success(msg: str, **kwargs):
+def success(msg: str, dedupe: bool = False, **kwargs):
     """Print a success message.
 
     Args:
         msg: The success message.
+        dedupe: If True, suppress multiple console logs of success message.
         kwargs: Keyword arguments to pass to the print function.
     """
     if _LOG_LEVEL <= LogLevel.INFO:
+        if dedupe:
+            if msg in _EMITTED_SUCCESS:
+                return
+            else:
+                _EMITTED_SUCCESS.add(msg)
         print(f"[green]Success: {msg}[/green]", **kwargs)
 
 
-def log(msg: str, **kwargs):
+def log(msg: str, dedupe: bool = False, **kwargs):
     """Takes a string and logs it to the console.
 
     Args:
         msg: The message to log.
+        dedupe: If True, suppress multiple console logs of log message.
         kwargs: Keyword arguments to pass to the print function.
     """
     if _LOG_LEVEL <= LogLevel.INFO:
+        if dedupe:
+            if msg in _EMITTED_LOGS:
+                return
+            else:
+                _EMITTED_LOGS.add(msg)
         _console.log(msg, **kwargs)
 
 
@@ -129,14 +171,20 @@ def rule(title: str, **kwargs):
     _console.rule(title, **kwargs)
 
 
-def warn(msg: str, **kwargs):
+def warn(msg: str, dedupe: bool = False, **kwargs):
     """Print a warning message.
 
     Args:
         msg: The warning message.
+        dedupe: If True, suppress multiple console logs of warning message.
         kwargs: Keyword arguments to pass to the print function.
     """
     if _LOG_LEVEL <= LogLevel.WARNING:
+        if dedupe:
+            if msg in _EMIITED_WARNINGS:
+                return
+            else:
+                _EMIITED_WARNINGS.add(msg)
         print(f"[orange1]Warning: {msg}[/orange1]", **kwargs)
 
 
@@ -169,14 +217,20 @@ def deprecate(
             _EMITTED_DEPRECATION_WARNINGS.add(feature_name)
 
 
-def error(msg: str, **kwargs):
+def error(msg: str, dedupe: bool = False, **kwargs):
     """Print an error message.
 
     Args:
         msg: The error message.
+        dedupe: If True, suppress multiple console logs of error message.
         kwargs: Keyword arguments to pass to the print function.
     """
     if _LOG_LEVEL <= LogLevel.ERROR:
+        if dedupe:
+            if msg in _EMITTED_ERRORS:
+                return
+            else:
+                _EMITTED_ERRORS.add(msg)
         print(f"[red]{msg}[/red]", **kwargs)
 
 

+ 4 - 0
reflex/utils/exceptions.py

@@ -183,3 +183,7 @@ def raise_system_package_missing_error(package: str) -> NoReturn:
         " Please install it through your system package manager."
         + (f" You can do so by running 'brew install {package}'." if IS_MACOS else "")
     )
+
+
+class InvalidLockWarningThresholdError(ReflexError):
+    """Raised when an invalid lock warning threshold is provided."""

+ 5 - 5
reflex/utils/exec.py

@@ -24,7 +24,7 @@ from reflex.utils.prerequisites import get_web_dir
 frontend_process = None
 
 
-def detect_package_change(json_file_path: str) -> str:
+def detect_package_change(json_file_path: Path) -> str:
     """Calculates the SHA-256 hash of a JSON file and returns it as a hexadecimal string.
 
     Args:
@@ -37,7 +37,7 @@ def detect_package_change(json_file_path: str) -> str:
         >>> detect_package_change("package.json")
         'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2'
     """
-    with open(json_file_path, "r") as file:
+    with json_file_path.open("r") as file:
         json_data = json.load(file)
 
     # Calculate the hash
@@ -81,7 +81,7 @@ def run_process_and_launch_url(run_command: list[str], backend_present=True):
     from reflex.utils import processes
 
     json_file_path = get_web_dir() / constants.PackageJson.PATH
-    last_hash = detect_package_change(str(json_file_path))
+    last_hash = detect_package_change(json_file_path)
     process = None
     first_run = True
 
@@ -117,14 +117,14 @@ def run_process_and_launch_url(run_command: list[str], backend_present=True):
                         console.print("New packages detected: Updating app...")
                 else:
                     if any(
-                        [x in line for x in ("bin executable does not exist on disk",)]
+                        x in line for x in ("bin executable does not exist on disk",)
                     ):
                         console.error(
                             "Try setting `REFLEX_USE_NPM=1` and re-running `reflex init` and `reflex run` to use npm instead of bun:\n"
                             "`REFLEX_USE_NPM=1 reflex init`\n"
                             "`REFLEX_USE_NPM=1 reflex run`"
                         )
-                    new_hash = detect_package_change(str(json_file_path))
+                    new_hash = detect_package_change(json_file_path)
                     if new_hash != last_hash:
                         last_hash = new_hash
                         kill(process.pid)

+ 1 - 2
reflex/utils/export.py

@@ -1,6 +1,5 @@
 """Export utilities."""
 
-import os
 from pathlib import Path
 from typing import Optional
 
@@ -15,7 +14,7 @@ def export(
     zipping: bool = True,
     frontend: bool = True,
     backend: bool = True,
-    zip_dest_dir: str = os.getcwd(),
+    zip_dest_dir: str = str(Path.cwd()),
     upload_db_file: bool = False,
     api_url: Optional[str] = None,
     deploy_url: Optional[str] = None,

+ 6 - 2
reflex/utils/format.py

@@ -664,18 +664,22 @@ def format_library_name(library_fullname: str):
     return lib
 
 
-def json_dumps(obj: Any) -> str:
+def json_dumps(obj: Any, **kwargs) -> str:
     """Takes an object and returns a jsonified string.
 
     Args:
         obj: The object to be serialized.
+        kwargs: Additional keyword arguments to pass to json.dumps.
 
     Returns:
         A string
     """
     from reflex.utils import serializers
 
-    return json.dumps(obj, ensure_ascii=False, default=serializers.serialize)
+    kwargs.setdefault("ensure_ascii", False)
+    kwargs.setdefault("default", serializers.serialize)
+
+    return json.dumps(obj, **kwargs)
 
 
 def collect_form_dict_names(form_dict: dict[str, Any]) -> dict[str, Any]:

+ 2 - 2
reflex/utils/path_ops.py

@@ -205,14 +205,14 @@ def update_json_file(file_path: str | Path, update_dict: dict[str, int | str]):
     # Read the existing json object from the file.
     json_object = {}
     if fp.stat().st_size:
-        with open(fp) as f:
+        with fp.open() as f:
             json_object = json.load(f)
 
     # Update the json object with the new data.
     json_object.update(update_dict)
 
     # Write the updated json object to the file
-    with open(fp, "w") as f:
+    with fp.open("w") as f:
         json.dump(json_object, f, ensure_ascii=False)
 
 

+ 45 - 29
reflex/utils/prerequisites.py

@@ -109,7 +109,7 @@ def check_latest_package_version(package_name: str):
             console.warn(
                 f"Your version ({current_version}) of {package_name} is out of date. Upgrade to {latest_version} with 'pip install {package_name} --upgrade'"
             )
-        # Check for depreacted python versions
+        # Check for deprecated python versions
         _python_version_check()
     except Exception:
         pass
@@ -290,7 +290,7 @@ def get_app(reload: bool = False) -> ModuleType:
                 "If this error occurs in a reflex test case, ensure that `get_app` is mocked."
             )
         module = config.module
-        sys.path.insert(0, os.getcwd())
+        sys.path.insert(0, str(Path.cwd()))
         app = __import__(module, fromlist=(constants.CompileVars.APP,))
 
         if reload:
@@ -372,16 +372,13 @@ def parse_redis_url() -> str | dict | None:
     return config.redis_url
 
 
-async def get_redis_status() -> bool | None:
+async def get_redis_status() -> dict[str, bool | None]:
     """Checks the status of the Redis connection.
 
     Attempts to connect to Redis and send a ping command to verify connectivity.
 
     Returns:
-        bool or None: The status of the Redis connection:
-            - True: Redis is accessible and responding.
-            - False: Redis is not accessible due to a connection error.
-            - None: Redis not used i.e redis_url is not set in rxconfig.
+        The status of the Redis connection.
     """
     try:
         status = True
@@ -393,7 +390,7 @@ async def get_redis_status() -> bool | None:
     except exceptions.RedisError:
         status = False
 
-    return status
+    return {"redis": status}
 
 
 def validate_app_name(app_name: str | None = None) -> str:
@@ -438,9 +435,11 @@ def create_config(app_name: str):
     from reflex.compiler import templates
 
     config_name = f"{re.sub(r'[^a-zA-Z]', '', app_name).capitalize()}Config"
-    with open(constants.Config.FILE, "w") as f:
-        console.debug(f"Creating {constants.Config.FILE}")
-        f.write(templates.RXCONFIG.render(app_name=app_name, config_name=config_name))
+
+    console.debug(f"Creating {constants.Config.FILE}")
+    constants.Config.FILE.write_text(
+        templates.RXCONFIG.render(app_name=app_name, config_name=config_name)
+    )
 
 
 def initialize_gitignore(
@@ -494,14 +493,14 @@ def initialize_requirements_txt():
         console.debug(f"Detected encoding for {fp} as {encoding}.")
     try:
         other_requirements_exist = False
-        with open(fp, "r", encoding=encoding) as f:
+        with fp.open("r", encoding=encoding) as f:
             for req in f:
                 # Check if we have a package name that is reflex
                 if re.match(r"^reflex[^a-zA-Z0-9]", req):
                     console.debug(f"{fp} already has reflex as dependency.")
                     return
                 other_requirements_exist = True
-        with open(fp, "a", encoding=encoding) as f:
+        with fp.open("a", encoding=encoding) as f:
             preceding_newline = "\n" if other_requirements_exist else ""
             f.write(
                 f"{preceding_newline}{constants.RequirementsTxt.DEFAULTS_STUB}{constants.Reflex.VERSION}\n"
@@ -592,7 +591,7 @@ def initialize_web_directory():
     """Initialize the web directory on reflex init."""
     console.log("Initializing the web directory.")
 
-    # Re-use the hash if one is already created, so we don't over-write it when running reflex init
+    # Reuse the hash if one is already created, so we don't over-write it when running reflex init
     project_hash = get_project_hash()
 
     path_ops.cp(constants.Templates.Dirs.WEB_TEMPLATE, str(get_web_dir()))
@@ -645,7 +644,7 @@ def initialize_bun_config():
 def init_reflex_json(project_hash: int | None):
     """Write the hash of the Reflex project to a REFLEX_JSON.
 
-    Re-use the hash if one is already created, therefore do not
+    Reuse the hash if one is already created, therefore do not
     overwrite it every time we run the reflex init command
     .
 
@@ -699,7 +698,7 @@ def _update_next_config(
     }
     if transpile_packages:
         next_config["transpilePackages"] = list(
-            set((format_library_name(p) for p in transpile_packages))
+            {format_library_name(p) for p in transpile_packages}
         )
     if export:
         next_config["output"] = "export"
@@ -732,13 +731,13 @@ def download_and_run(url: str, *args, show_status: bool = False, **env):
         response.raise_for_status()
 
     # Save the script to a temporary file.
-    script = tempfile.NamedTemporaryFile()
-    with open(script.name, "w") as f:
-        f.write(response.text)
+    script = Path(tempfile.NamedTemporaryFile().name)
+
+    script.write_text(response.text)
 
     # Run the script.
     env = {**os.environ, **env}
-    process = processes.new_process(["bash", f.name, *args], env=env)
+    process = processes.new_process(["bash", str(script), *args], env=env)
     show = processes.show_status if show_status else processes.show_logs
     show(f"Installing {url}", process)
 
@@ -752,14 +751,14 @@ def download_and_extract_fnm_zip():
     # Download the zip file
     url = constants.Fnm.INSTALL_URL
     console.debug(f"Downloading {url}")
-    fnm_zip_file = constants.Fnm.DIR / f"{constants.Fnm.FILENAME}.zip"
+    fnm_zip_file: Path = constants.Fnm.DIR / f"{constants.Fnm.FILENAME}.zip"
     # Function to download and extract the FNM zip release.
     try:
         # Download the FNM zip release.
         # TODO: show progress to improve UX
         response = net.get(url, follow_redirects=True)
         response.raise_for_status()
-        with open(fnm_zip_file, "wb") as output_file:
+        with fnm_zip_file.open("wb") as output_file:
             for chunk in response.iter_bytes():
                 output_file.write(chunk)
 
@@ -807,7 +806,7 @@ def install_node():
         )
     else:  # All other platforms (Linux, MacOS).
         # Add execute permissions to fnm executable.
-        os.chmod(constants.Fnm.EXE, stat.S_IXUSR)
+        constants.Fnm.EXE.chmod(stat.S_IXUSR)
         # Install node.
         # Specify arm64 arch explicitly for M1s and M2s.
         architecture_arg = (
@@ -925,7 +924,7 @@ def cached_procedure(cache_file: str, payload_fn: Callable[..., str]):
 
 @cached_procedure(
     cache_file=str(get_web_dir() / "reflex.install_frontend_packages.cached"),
-    payload_fn=lambda p, c: f"{sorted(list(p))!r},{c.json()}",
+    payload_fn=lambda p, c: f"{sorted(p)!r},{c.json()}",
 )
 def install_frontend_packages(packages: set[str], config: Config):
     """Installs the base and custom frontend packages.
@@ -1175,6 +1174,24 @@ def initialize_frontend_dependencies():
     initialize_web_directory()
 
 
+def check_db_used() -> bool:
+    """Check if the database is used.
+
+    Returns:
+        True if the database is used.
+    """
+    return bool(get_config().db_url)
+
+
+def check_redis_used() -> bool:
+    """Check if Redis is used.
+
+    Returns:
+        True if Redis is used.
+    """
+    return bool(get_config().redis_url)
+
+
 def check_db_initialized() -> bool:
     """Check if the database migrations are initialized.
 
@@ -1300,7 +1317,7 @@ def fetch_app_templates(version: str) -> dict[str, Template]:
     for tp in templates_data:
         if tp["hidden"] or tp["code_url"] is None:
             continue
-        known_fields = set(f.name for f in dataclasses.fields(Template))
+        known_fields = {f.name for f in dataclasses.fields(Template)}
         filtered_templates[tp["name"]] = Template(
             **{k: v for k, v in tp.items() if k in known_fields}
         )
@@ -1326,7 +1343,7 @@ def create_config_init_app_from_remote_template(app_name: str, template_url: str
         raise typer.Exit(1) from ose
 
     # Use httpx GET with redirects to download the zip file.
-    zip_file_path = Path(temp_dir) / "template.zip"
+    zip_file_path: Path = Path(temp_dir) / "template.zip"
     try:
         # Note: following redirects can be risky. We only allow this for reflex built templates at the moment.
         response = net.get(template_url, follow_redirects=True)
@@ -1336,9 +1353,8 @@ def create_config_init_app_from_remote_template(app_name: str, template_url: str
         console.error(f"Failed to download the template: {he}")
         raise typer.Exit(1) from he
     try:
-        with open(zip_file_path, "wb") as f:
-            f.write(response.content)
-            console.debug(f"Downloaded the zip to {zip_file_path}")
+        zip_file_path.write_bytes(response.content)
+        console.debug(f"Downloaded the zip to {zip_file_path}")
     except OSError as ose:
         console.error(f"Unable to write the downloaded zip to disk {ose}")
         raise typer.Exit(1) from ose

+ 5 - 5
reflex/utils/processes.py

@@ -58,7 +58,9 @@ def get_process_on_port(port) -> Optional[psutil.Process]:
         The process on the given port.
     """
     for proc in psutil.process_iter(["pid", "name", "cmdline"]):
-        try:
+        with contextlib.suppress(
+            psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess
+        ):
             if importlib.metadata.version("psutil") >= "6.0.0":
                 conns = proc.net_connections(kind="inet")  # type: ignore
             else:
@@ -66,8 +68,6 @@ def get_process_on_port(port) -> Optional[psutil.Process]:
             for conn in conns:
                 if conn.laddr.port == int(port):
                     return proc
-        except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
-            pass
     return None
 
 
@@ -118,7 +118,7 @@ def handle_port(service_name: str, port: str, default_port: str) -> str:
     """Change port if the specified port is in use and is not explicitly specified as a CLI arg or config arg.
     otherwise tell the user the port is in use and exit the app.
 
-    We make an assumption that when port is the default port,then it hasnt been explicitly set since its not straightforward
+    We make an assumption that when port is the default port,then it hasn't been explicitly set since its not straightforward
     to know whether a port was explicitly provided by the user unless its any other than the default.
 
     Args:
@@ -351,7 +351,7 @@ def atexit_handler():
 
 def get_command_with_loglevel(command: list[str]) -> list[str]:
     """Add the right loglevel flag to the designated command.
-     npm uses --loglevel <level>, Bun doesnt use the --loglevel flag and
+     npm uses --loglevel <level>, Bun doesn't use the --loglevel flag and
      runs in debug mode by default.
 
     Args:

+ 4 - 5
reflex/utils/pyi_generator.py

@@ -287,10 +287,9 @@ def _generate_docstrings(clzs: list[Type[Component]], props: list[str]) -> str:
     for line in (clz.create.__doc__ or "").splitlines():
         if "**" in line:
             indent = line.split("**")[0]
-            for nline in [
-                f"{indent}{n}:{' '.join(c)}" for n, c in props_comments.items()
-            ]:
-                new_docstring.append(nline)
+            new_docstring.extend(
+                [f"{indent}{n}:{' '.join(c)}" for n, c in props_comments.items()]
+            )
         new_docstring.append(line)
     return "\n".join(new_docstring)
 
@@ -1024,7 +1023,7 @@ class InitStubGenerator(StubGenerator):
 
 class PyiGenerator:
     """A .pyi file generator that will scan all defined Component in Reflex and
-    generate the approriate stub.
+    generate the appropriate stub.
     """
 
     modules: list = []

+ 6 - 4
reflex/utils/telemetry.py

@@ -7,6 +7,7 @@ import dataclasses
 import multiprocessing
 import platform
 import warnings
+from contextlib import suppress
 
 from reflex.config import environment
 
@@ -171,10 +172,11 @@ def _send(event, telemetry_enabled, **kwargs):
     if not telemetry_enabled:
         return False
 
-    event_data = _prepare_event(event, **kwargs)
-    if not event_data:
-        return False
-    return _send_event(event_data)
+    with suppress(Exception):
+        event_data = _prepare_event(event, **kwargs)
+        if not event_data:
+            return False
+        return _send_event(event_data)
 
 
 def send(event: str, telemetry_enabled: bool | None = None, **kwargs):

+ 1 - 0
reflex/vars/__init__.py

@@ -9,6 +9,7 @@ from .base import get_unique_variable_name as get_unique_variable_name
 from .base import get_uuid_string_var as get_uuid_string_var
 from .base import var_operation as var_operation
 from .base import var_operation_return as var_operation_return
+from .datetime import DateTimeVar as DateTimeVar
 from .function import FunctionStringVar as FunctionStringVar
 from .function import FunctionVar as FunctionVar
 from .function import VarOperationCall as VarOperationCall

+ 37 - 27
reflex/vars/base.py

@@ -50,7 +50,8 @@ from typing_extensions import (
 
 from reflex import constants
 from reflex.base import Base
-from reflex.utils import console, imports, serializers, types
+from reflex.constants.compiler import Hooks
+from reflex.utils import console, exceptions, imports, serializers, types
 from reflex.utils.exceptions import (
     VarAttributeError,
     VarDependencyError,
@@ -170,6 +171,12 @@ class VarData:
     # Components that need to be present in the component to render this var
     components: Tuple[BaseComponent, ...] = dataclasses.field(default_factory=tuple)
 
+    # Dependencies of the var
+    deps: Tuple[Var, ...] = dataclasses.field(default_factory=tuple)
+
+    # Position of the hook in the component
+    position: Hooks.HookPosition | None = None
+
     def __init__(
         self,
         state: str = "",
@@ -177,6 +184,8 @@ class VarData:
         imports: ImportDict | ParsedImportDict | None = None,
         hooks: dict[str, None] | None = None,
         components: Iterable[BaseComponent] | None = None,
+        deps: list[Var] | None = None,
+        position: Hooks.HookPosition | None = None,
     ):
         """Initialize the var data.
 
@@ -186,6 +195,8 @@ class VarData:
             imports: Imports needed to render this var.
             hooks: Hooks that need to be present in the component to render this var.
             components: Components that need to be present in the component to render this var.
+            deps: Dependencies of the var for useCallback.
+            position: Position of the hook in the component.
         """
         immutable_imports: ImmutableParsedImportDict = tuple(
             sorted(
@@ -199,6 +210,8 @@ class VarData:
         object.__setattr__(
             self, "components", tuple(components) if components is not None else tuple()
         )
+        object.__setattr__(self, "deps", tuple(deps or []))
+        object.__setattr__(self, "position", position or None)
 
     def old_school_imports(self) -> ImportDict:
         """Return the imports as a mutable dict.
@@ -206,7 +219,7 @@ class VarData:
         Returns:
             The imports as a mutable dict.
         """
-        return dict((k, list(v)) for k, v in self.imports)
+        return {k: list(v) for k, v in self.imports}
 
     def merge(*all: VarData | None) -> VarData | None:
         """Merge multiple var data objects.
@@ -214,6 +227,9 @@ class VarData:
         Args:
             *all: The var data objects to merge.
 
+        Raises:
+            ReflexError: If trying to merge VarData with different positions.
+
         Returns:
             The merged var data object.
 
@@ -544,7 +560,6 @@ class Var(Generic[VAR_TYPE]):
             raise TypeError(
                 "The _var_full_name_needs_state_prefix argument is not supported for Var."
             )
-
         value_with_replaced = dataclasses.replace(
             self,
             _var_type=_var_type or self._var_type,
@@ -1878,14 +1893,12 @@ class CachedVarOperation:
             The cached VarData.
         """
         return VarData.merge(
-            *map(
-                lambda value: (
-                    value._get_all_var_data() if isinstance(value, Var) else None
-                ),
-                map(
-                    lambda field: getattr(self, field.name),
-                    dataclasses.fields(self),  # type: ignore
-                ),
+            *(
+                value._get_all_var_data() if isinstance(value, Var) else None
+                for value in (
+                    getattr(self, field.name)
+                    for field in dataclasses.fields(self)  # type: ignore
+                )
             ),
             self._var_data,
         )
@@ -2116,20 +2129,20 @@ class ComputedVar(Var[RETURN_TYPE]):
         Raises:
             TypeError: If kwargs contains keys that are not allowed.
         """
-        field_values = dict(
-            fget=kwargs.pop("fget", self._fget),
-            initial_value=kwargs.pop("initial_value", self._initial_value),
-            cache=kwargs.pop("cache", self._cache),
-            deps=kwargs.pop("deps", self._static_deps),
-            auto_deps=kwargs.pop("auto_deps", self._auto_deps),
-            interval=kwargs.pop("interval", self._update_interval),
-            backend=kwargs.pop("backend", self._backend),
-            _js_expr=kwargs.pop("_js_expr", self._js_expr),
-            _var_type=kwargs.pop("_var_type", self._var_type),
-            _var_data=kwargs.pop(
+        field_values = {
+            "fget": kwargs.pop("fget", self._fget),
+            "initial_value": kwargs.pop("initial_value", self._initial_value),
+            "cache": kwargs.pop("cache", self._cache),
+            "deps": kwargs.pop("deps", self._static_deps),
+            "auto_deps": kwargs.pop("auto_deps", self._auto_deps),
+            "interval": kwargs.pop("interval", self._update_interval),
+            "backend": kwargs.pop("backend", self._backend),
+            "_js_expr": kwargs.pop("_js_expr", self._js_expr),
+            "_var_type": kwargs.pop("_var_type", self._var_type),
+            "_var_data": kwargs.pop(
                 "_var_data", VarData.merge(self._var_data, merge_var_data)
             ),
-        )
+        }
 
         if kwargs:
             unexpected_kwargs = ", ".join(kwargs.keys())
@@ -2604,10 +2617,7 @@ class CustomVarOperation(CachedVarOperation, Var[T]):
             The cached VarData.
         """
         return VarData.merge(
-            *map(
-                lambda arg: arg[1]._get_all_var_data(),
-                self._args,
-            ),
+            *(arg[1]._get_all_var_data() for arg in self._args),
             self._return._get_all_var_data(),
             self._var_data,
         )

+ 222 - 0
reflex/vars/datetime.py

@@ -0,0 +1,222 @@
+"""Immutable datetime and date vars."""
+
+from __future__ import annotations
+
+import dataclasses
+import sys
+from datetime import date, datetime
+from typing import Any, NoReturn, TypeVar, Union, overload
+
+from reflex.utils.exceptions import VarTypeError
+from reflex.vars.number import BooleanVar
+
+from .base import (
+    CustomVarOperationReturn,
+    LiteralVar,
+    Var,
+    VarData,
+    var_operation,
+    var_operation_return,
+)
+
+DATETIME_T = TypeVar("DATETIME_T", datetime, date)
+
+datetime_types = Union[datetime, date]
+
+
+def raise_var_type_error():
+    """Raise a VarTypeError.
+
+    Raises:
+        VarTypeError: Cannot compare a datetime object with a non-datetime object.
+    """
+    raise VarTypeError("Cannot compare a datetime object with a non-datetime object.")
+
+
+class DateTimeVar(Var[DATETIME_T], python_types=(datetime, date)):
+    """A variable that holds a datetime or date object."""
+
+    @overload
+    def __lt__(self, other: datetime_types) -> BooleanVar: ...
+
+    @overload
+    def __lt__(self, other: NoReturn) -> NoReturn: ...
+
+    def __lt__(self, other: Any):
+        """Less than comparison.
+
+        Args:
+            other: The other datetime to compare.
+
+        Returns:
+            The result of the comparison.
+        """
+        if not isinstance(other, DATETIME_TYPES):
+            raise_var_type_error()
+        return date_lt_operation(self, other)
+
+    @overload
+    def __le__(self, other: datetime_types) -> BooleanVar: ...
+
+    @overload
+    def __le__(self, other: NoReturn) -> NoReturn: ...
+
+    def __le__(self, other: Any):
+        """Less than or equal comparison.
+
+        Args:
+            other: The other datetime to compare.
+
+        Returns:
+            The result of the comparison.
+        """
+        if not isinstance(other, DATETIME_TYPES):
+            raise_var_type_error()
+        return date_le_operation(self, other)
+
+    @overload
+    def __gt__(self, other: datetime_types) -> BooleanVar: ...
+
+    @overload
+    def __gt__(self, other: NoReturn) -> NoReturn: ...
+
+    def __gt__(self, other: Any):
+        """Greater than comparison.
+
+        Args:
+            other: The other datetime to compare.
+
+        Returns:
+            The result of the comparison.
+        """
+        if not isinstance(other, DATETIME_TYPES):
+            raise_var_type_error()
+        return date_gt_operation(self, other)
+
+    @overload
+    def __ge__(self, other: datetime_types) -> BooleanVar: ...
+
+    @overload
+    def __ge__(self, other: NoReturn) -> NoReturn: ...
+
+    def __ge__(self, other: Any):
+        """Greater than or equal comparison.
+
+        Args:
+            other: The other datetime to compare.
+
+        Returns:
+            The result of the comparison.
+        """
+        if not isinstance(other, DATETIME_TYPES):
+            raise_var_type_error()
+        return date_ge_operation(self, other)
+
+
+@var_operation
+def date_gt_operation(lhs: Var | Any, rhs: Var | Any) -> CustomVarOperationReturn:
+    """Greater than comparison.
+
+    Args:
+        lhs: The left-hand side of the operation.
+        rhs: The right-hand side of the operation.
+
+    Returns:
+        The result of the operation.
+    """
+    return date_compare_operation(rhs, lhs, strict=True)
+
+
+@var_operation
+def date_lt_operation(lhs: Var | Any, rhs: Var | Any) -> CustomVarOperationReturn:
+    """Less than comparison.
+
+    Args:
+        lhs: The left-hand side of the operation.
+        rhs: The right-hand side of the operation.
+
+    Returns:
+        The result of the operation.
+    """
+    return date_compare_operation(lhs, rhs, strict=True)
+
+
+@var_operation
+def date_le_operation(lhs: Var | Any, rhs: Var | Any) -> CustomVarOperationReturn:
+    """Less than or equal comparison.
+
+    Args:
+        lhs: The left-hand side of the operation.
+        rhs: The right-hand side of the operation.
+
+    Returns:
+        The result of the operation.
+    """
+    return date_compare_operation(lhs, rhs)
+
+
+@var_operation
+def date_ge_operation(lhs: Var | Any, rhs: Var | Any) -> CustomVarOperationReturn:
+    """Greater than or equal comparison.
+
+    Args:
+        lhs: The left-hand side of the operation.
+        rhs: The right-hand side of the operation.
+
+    Returns:
+        The result of the operation.
+    """
+    return date_compare_operation(rhs, lhs)
+
+
+def date_compare_operation(
+    lhs: DateTimeVar[DATETIME_T] | Any,
+    rhs: DateTimeVar[DATETIME_T] | Any,
+    strict: bool = False,
+) -> CustomVarOperationReturn:
+    """Check if the value is less than the other value.
+
+    Args:
+        lhs: The left-hand side of the operation.
+        rhs: The right-hand side of the operation.
+        strict: Whether to use strict comparison.
+
+    Returns:
+        The result of the operation.
+    """
+    return var_operation_return(
+        f"({lhs} { '<' if strict else '<='} {rhs})",
+        bool,
+    )
+
+
+@dataclasses.dataclass(
+    eq=False,
+    frozen=True,
+    **{"slots": True} if sys.version_info >= (3, 10) else {},
+)
+class LiteralDatetimeVar(LiteralVar, DateTimeVar):
+    """Base class for immutable datetime and date vars."""
+
+    _var_value: datetime | date = dataclasses.field(default=datetime.now())
+
+    @classmethod
+    def create(cls, value: datetime | date, _var_data: VarData | None = None):
+        """Create a new instance of the class.
+
+        Args:
+            value: The value to set.
+
+        Returns:
+            LiteralDatetimeVar: The new instance of the class.
+        """
+        js_expr = f'"{value!s}"'
+        return cls(
+            _js_expr=js_expr,
+            _var_type=type(value),
+            _var_value=value,
+            _var_data=_var_data,
+        )
+
+
+DATETIME_TYPES = (datetime, date, DateTimeVar)

+ 2 - 2
reflex/vars/function.py

@@ -547,7 +547,7 @@ class VarOperationCall(Generic[P, R], CachedVarOperation, Var[R]):
 class DestructuredArg:
     """Class for destructured arguments."""
 
-    fields: Tuple[str, ...] = tuple()
+    fields: Tuple[str, ...] = ()
     rest: Optional[str] = None
 
     def to_javascript(self) -> str:
@@ -569,7 +569,7 @@ class DestructuredArg:
 class FunctionArgs:
     """Class for function arguments."""
 
-    args: Tuple[Union[str, DestructuredArg], ...] = tuple()
+    args: Tuple[Union[str, DestructuredArg], ...] = ()
     rest: Optional[str] = None
 
 

+ 1 - 1
reflex/vars/number.py

@@ -47,7 +47,7 @@ def raise_unsupported_operand_types(
         VarTypeError: The operand types are unsupported.
     """
     raise VarTypeError(
-        f"Unsupported Operand type(s) for {operator}: {', '.join(map(lambda t: t.__name__, operands_types))}"
+        f"Unsupported Operand type(s) for {operator}: {', '.join(t.__name__ for t in operands_types)}"
     )
 
 

+ 1 - 1
reflex/vars/sequence.py

@@ -918,7 +918,7 @@ class ArrayVar(Var[ARRAY_VAR_TYPE], python_types=(Sequence, set)):
         if num_args == 0:
             return_value = fn()  # type: ignore
             simple_function_var: FunctionVar[ReflexCallable[[], ANOTHER_ARRAY_VAR]] = (
-                ArgsFunctionOperation.create(tuple(), return_value)
+                ArgsFunctionOperation.create((), return_value)
             )
             return map_array_operation(self, simple_function_var).guess_type()
 

+ 4 - 5
scripts/wait_for_listening_port.py

@@ -49,11 +49,10 @@ def main():
     parser.add_argument("--server-pid", type=int)
     args = parser.parse_args()
     executor = ThreadPoolExecutor(max_workers=len(args.port))
-    futures = []
-    for p in args.port:
-        futures.append(
-            executor.submit(_wait_for_port, p, args.server_pid, args.timeout)
-        )
+    futures = [
+        executor.submit(_wait_for_port, p, args.server_pid, args.timeout)
+        for p in args.port
+    ]
     for f in as_completed(futures):
         ok, msg = f.result()
         if ok:

+ 23 - 0
tests/integration/conftest.py

@@ -6,6 +6,7 @@ from pathlib import Path
 
 import pytest
 
+import reflex.app
 from reflex.config import environment
 from reflex.testing import AppHarness, AppHarnessProd
 
@@ -76,3 +77,25 @@ def app_harness_env(request):
         The AppHarness class to use for the test.
     """
     return request.param
+
+
+@pytest.fixture(autouse=True)
+def raise_console_error(request, mocker):
+    """Spy on calls to `console.error` used by the framework.
+
+    Help catch spurious error conditions that might otherwise go unnoticed.
+
+    If a test is marked with `ignore_console_error`, the spy will be ignored
+    after the test.
+
+    Args:
+        request: The pytest request object.
+        mocker: The pytest mocker object.
+
+    Yields:
+        control to the test function.
+    """
+    spy = mocker.spy(reflex.app.console, "error")
+    yield
+    if "ignore_console_error" not in request.keywords:
+        spy.assert_not_called()

+ 2 - 2
tests/integration/test_call_script.py

@@ -15,6 +15,7 @@ from .utils import SessionStorage
 
 def CallScript():
     """A test app for browser javascript integration."""
+    from pathlib import Path
     from typing import Dict, List, Optional, Union
 
     import reflex as rx
@@ -186,8 +187,7 @@ def CallScript():
             self.reset()
 
     app = rx.App(state=rx.State)
-    with open("assets/external.js", "w") as f:
-        f.write(external_scripts)
+    Path("assets/external.js").write_text(external_scripts)
 
     @app.add_page
     def index():

+ 4 - 5
tests/integration/test_client_storage.py

@@ -637,8 +637,7 @@ async def test_client_side_state(
     assert await AppHarness._poll_for_async(poll_for_not_hydrated)
 
     # Trigger event to get a new instance of the state since the old was expired.
-    state_var_input = driver.find_element(By.ID, "state_var")
-    state_var_input.send_keys("re-triggering")
+    set_sub("c1", "c1 post expire")
 
     # get new references to all cookie and local storage elements (again)
     c1 = driver.find_element(By.ID, "c1")
@@ -659,7 +658,7 @@ async def test_client_side_state(
     l1s = driver.find_element(By.ID, "l1s")
     s1s = driver.find_element(By.ID, "s1s")
 
-    assert c1.text == "c1 value"
+    assert c1.text == "c1 post expire"
     assert c2.text == "c2 value"
     assert c3.text == ""  # temporary cookie expired after reset state!
     assert c4.text == "c4 value"
@@ -690,11 +689,11 @@ async def test_client_side_state(
 
     async def poll_for_c1_set():
         sub_state = await get_sub_state()
-        return sub_state.c1 == "c1 value"
+        return sub_state.c1 == "c1 post expire"
 
     assert await AppHarness._poll_for_async(poll_for_c1_set)
     sub_state = await get_sub_state()
-    assert sub_state.c1 == "c1 value"
+    assert sub_state.c1 == "c1 post expire"
     assert sub_state.c2 == "c2 value"
     assert sub_state.c3 == ""
     assert sub_state.c4 == "c4 value"

+ 2 - 0
tests/integration/test_exception_handlers.py

@@ -13,6 +13,8 @@ from selenium.webdriver.support.ui import WebDriverWait
 
 from reflex.testing import AppHarness, AppHarnessProd
 
+pytestmark = [pytest.mark.ignore_console_error]
+
 
 def TestApp():
     """A test app for event exception handler integration."""

+ 15 - 2
tests/integration/test_upload.py

@@ -381,9 +381,22 @@ async def test_cancel_upload(tmp_path, upload_file: AppHarness, driver: WebDrive
     await asyncio.sleep(0.3)
     cancel_button.click()
 
-    # look up the backend state and assert on progress
+    # Wait a bit for the upload to get cancelled.
+    await asyncio.sleep(0.5)
+
+    # Get interim progress dicts saved in the on_upload_progress handler.
+    async def _progress_dicts():
+        state = await upload_file.get_state(substate_token)
+        return state.substates[state_name].progress_dicts
+
+    # We should have _some_ progress
+    assert await AppHarness._poll_for_async(_progress_dicts)
+
+    # But there should never be a final progress record for a cancelled upload.
+    for p in await _progress_dicts():
+        assert p["progress"] != 1
+
     state = await upload_file.get_state(substate_token)
-    assert state.substates[state_name].progress_dicts
     file_data = state.substates[state_name]._file_data
     assert isinstance(file_data, dict)
     normalized_file_data = {Path(k).name: v for k, v in file_data.items()}

+ 87 - 0
tests/integration/tests_playwright/test_datetime_operations.py

@@ -0,0 +1,87 @@
+from typing import Generator
+
+import pytest
+from playwright.sync_api import Page, expect
+
+from reflex.testing import AppHarness
+
+
+def DatetimeOperationsApp():
+    from datetime import datetime
+
+    import reflex as rx
+
+    class DtOperationsState(rx.State):
+        date1: datetime = datetime(2021, 1, 1)
+        date2: datetime = datetime(2031, 1, 1)
+        date3: datetime = datetime(2021, 1, 1)
+
+    app = rx.App(state=DtOperationsState)
+
+    @app.add_page
+    def index():
+        return rx.vstack(
+            rx.text(DtOperationsState.date1, id="date1"),
+            rx.text(DtOperationsState.date2, id="date2"),
+            rx.text(DtOperationsState.date3, id="date3"),
+            rx.text("Operations between date1 and date2"),
+            rx.text(DtOperationsState.date1 == DtOperationsState.date2, id="1_eq_2"),
+            rx.text(DtOperationsState.date1 != DtOperationsState.date2, id="1_neq_2"),
+            rx.text(DtOperationsState.date1 < DtOperationsState.date2, id="1_lt_2"),
+            rx.text(DtOperationsState.date1 <= DtOperationsState.date2, id="1_le_2"),
+            rx.text(DtOperationsState.date1 > DtOperationsState.date2, id="1_gt_2"),
+            rx.text(DtOperationsState.date1 >= DtOperationsState.date2, id="1_ge_2"),
+            rx.text("Operations between date1 and date3"),
+            rx.text(DtOperationsState.date1 == DtOperationsState.date3, id="1_eq_3"),
+            rx.text(DtOperationsState.date1 != DtOperationsState.date3, id="1_neq_3"),
+            rx.text(DtOperationsState.date1 < DtOperationsState.date3, id="1_lt_3"),
+            rx.text(DtOperationsState.date1 <= DtOperationsState.date3, id="1_le_3"),
+            rx.text(DtOperationsState.date1 > DtOperationsState.date3, id="1_gt_3"),
+            rx.text(DtOperationsState.date1 >= DtOperationsState.date3, id="1_ge_3"),
+        )
+
+
+@pytest.fixture()
+def datetime_operations_app(tmp_path_factory) -> Generator[AppHarness, None, None]:
+    """Start Table app at tmp_path via AppHarness.
+
+    Args:
+        tmp_path_factory: pytest tmp_path_factory fixture
+
+    Yields:
+        running AppHarness instance
+
+    """
+    with AppHarness.create(
+        root=tmp_path_factory.mktemp("datetime_operations_app"),
+        app_source=DatetimeOperationsApp,  # type: ignore
+    ) as harness:
+        assert harness.app_instance is not None, "app is not running"
+        yield harness
+
+
+def test_datetime_operations(datetime_operations_app: AppHarness, page: Page):
+    assert datetime_operations_app.frontend_url is not None
+
+    page.goto(datetime_operations_app.frontend_url)
+    expect(page).to_have_url(datetime_operations_app.frontend_url + "/")
+    # Check the actual values
+    expect(page.locator("id=date1")).to_have_text("2021-01-01 00:00:00")
+    expect(page.locator("id=date2")).to_have_text("2031-01-01 00:00:00")
+    expect(page.locator("id=date3")).to_have_text("2021-01-01 00:00:00")
+
+    # Check the operations between date1 and date2
+    expect(page.locator("id=1_eq_2")).to_have_text("false")
+    expect(page.locator("id=1_neq_2")).to_have_text("true")
+    expect(page.locator("id=1_lt_2")).to_have_text("true")
+    expect(page.locator("id=1_le_2")).to_have_text("true")
+    expect(page.locator("id=1_gt_2")).to_have_text("false")
+    expect(page.locator("id=1_ge_2")).to_have_text("false")
+
+    # Check the operations between date1 and date3
+    expect(page.locator("id=1_eq_3")).to_have_text("true")
+    expect(page.locator("id=1_neq_3")).to_have_text("false")
+    expect(page.locator("id=1_lt_3")).to_have_text("false")
+    expect(page.locator("id=1_le_3")).to_have_text("true")
+    expect(page.locator("id=1_gt_3")).to_have_text("false")
+    expect(page.locator("id=1_ge_3")).to_have_text("true")

+ 3 - 3
tests/units/components/core/test_banner.py

@@ -12,7 +12,7 @@ def test_websocket_target_url():
     url = WebsocketTargetURL.create()
     var_data = url._get_all_var_data()
     assert var_data is not None
-    assert sorted(tuple((key for key, _ in var_data.imports))) == sorted(
+    assert sorted(key for key, _ in var_data.imports) == sorted(
         ("$/utils/state", "$/env.json")
     )
 
@@ -20,7 +20,7 @@ def test_websocket_target_url():
 def test_connection_banner():
     banner = ConnectionBanner.create()
     _imports = banner._get_all_imports(collapse=True)
-    assert sorted(tuple(_imports)) == sorted(
+    assert sorted(_imports) == sorted(
         (
             "react",
             "$/utils/context",
@@ -38,7 +38,7 @@ def test_connection_banner():
 def test_connection_modal():
     modal = ConnectionModal.create()
     _imports = modal._get_all_imports(collapse=True)
-    assert sorted(tuple(_imports)) == sorted(
+    assert sorted(_imports) == sorted(
         (
             "react",
             "$/utils/context",

+ 1 - 1
tests/units/components/core/test_cond.py

@@ -135,7 +135,7 @@ def test_cond_computed_var():
 
     comp = cond(True, CondStateComputed.computed_int, CondStateComputed.computed_str)
 
-    # TODO: shouln't this be a ComputedVar?
+    # TODO: shouldn't this be a ComputedVar?
     assert isinstance(comp, Var)
 
     state_name = format_state_name(CondStateComputed.get_full_name())

+ 18 - 0
tests/units/components/core/test_foreach.py

@@ -1,8 +1,10 @@
 from typing import Dict, List, Sequence, Set, Tuple, Union
 
+import pydantic.v1
 import pytest
 
 from reflex import el
+from reflex.base import Base
 from reflex.components.component import Component
 from reflex.components.core.foreach import (
     Foreach,
@@ -18,6 +20,12 @@ from reflex.vars.number import NumberVar
 from reflex.vars.sequence import ArrayVar
 
 
+class ForEachTag(Base):
+    """A tag for testing the ForEach component."""
+
+    name: str = ""
+
+
 class ForEachState(BaseState):
     """A state for testing the ForEach component."""
 
@@ -46,6 +54,8 @@ class ForEachState(BaseState):
     bad_annotation_list: list = [["red", "orange"], ["yellow", "blue"]]
     color_index_tuple: Tuple[int, str] = (0, "red")
 
+    default_factory_list: list[ForEachTag] = pydantic.v1.Field(default_factory=list)
+
 
 class ComponentStateTest(ComponentState):
     """A test component state."""
@@ -292,3 +302,11 @@ def test_foreach_component_state():
             ForEachState.colors_list,
             ComponentStateTest.create,
         )
+
+
+def test_foreach_default_factory():
+    """Test that the default factory is called."""
+    _ = Foreach.create(
+        ForEachState.default_factory_list,
+        lambda tag: text(tag.name),
+    )

+ 9 - 12
tests/units/states/upload.py

@@ -61,14 +61,13 @@ class FileUploadState(State):
         """
         for file in files:
             upload_data = await file.read()
-            outfile = f"{self._tmp_path}/{file.filename}"
+            assert file.filename is not None
+            outfile = self._tmp_path / file.filename
 
             # Save the file.
-            with open(outfile, "wb") as file_object:
-                file_object.write(upload_data)
+            outfile.write_bytes(upload_data)
 
             # Update the img var.
-            assert file.filename is not None
             self.img_list.append(file.filename)
 
     @rx.event(background=True)
@@ -109,14 +108,13 @@ class ChildFileUploadState(FileStateBase1):
         """
         for file in files:
             upload_data = await file.read()
-            outfile = f"{self._tmp_path}/{file.filename}"
+            assert file.filename is not None
+            outfile = self._tmp_path / file.filename
 
             # Save the file.
-            with open(outfile, "wb") as file_object:
-                file_object.write(upload_data)
+            outfile.write_bytes(upload_data)
 
             # Update the img var.
-            assert file.filename is not None
             self.img_list.append(file.filename)
 
     @rx.event(background=True)
@@ -157,14 +155,13 @@ class GrandChildFileUploadState(FileStateBase2):
         """
         for file in files:
             upload_data = await file.read()
-            outfile = f"{self._tmp_path}/{file.filename}"
+            assert file.filename is not None
+            outfile = self._tmp_path / file.filename
 
             # Save the file.
-            with open(outfile, "wb") as file_object:
-                file_object.write(upload_data)
+            outfile.write_bytes(upload_data)
 
             # Update the img var.
-            assert file.filename is not None
             self.img_list.append(file.filename)
 
     @rx.event(background=True)

+ 9 - 9
tests/units/test_db_config.py

@@ -164,7 +164,7 @@ def test_constructor_postgresql(username, password, host, port, database, expect
             "localhost",
             5432,
             "db",
-            "postgresql+psycopg2://user:pass@localhost:5432/db",
+            "postgresql+psycopg://user:pass@localhost:5432/db",
         ),
         (
             "user",
@@ -172,17 +172,17 @@ def test_constructor_postgresql(username, password, host, port, database, expect
             "localhost",
             None,
             "db",
-            "postgresql+psycopg2://user@localhost/db",
+            "postgresql+psycopg://user@localhost/db",
         ),
-        ("user", "", "", None, "db", "postgresql+psycopg2://user@/db"),
-        ("", "", "localhost", 5432, "db", "postgresql+psycopg2://localhost:5432/db"),
-        ("", "", "", None, "db", "postgresql+psycopg2:///db"),
+        ("user", "", "", None, "db", "postgresql+psycopg://user@/db"),
+        ("", "", "localhost", 5432, "db", "postgresql+psycopg://localhost:5432/db"),
+        ("", "", "", None, "db", "postgresql+psycopg:///db"),
     ],
 )
-def test_constructor_postgresql_psycopg2(
+def test_constructor_postgresql_psycopg(
     username, password, host, port, database, expected_url
 ):
-    """Test DBConfig.postgresql_psycopg2 constructor creates the instance correctly.
+    """Test DBConfig.postgresql_psycopg constructor creates the instance correctly.
 
     Args:
         username: Database username.
@@ -192,10 +192,10 @@ def test_constructor_postgresql_psycopg2(
         database: Database name.
         expected_url: Expected database URL generated.
     """
-    db_config = DBConfig.postgresql_psycopg2(
+    db_config = DBConfig.postgresql_psycopg(
         username=username, password=password, host=host, port=port, database=database
     )
-    assert db_config.engine == "postgresql+psycopg2"
+    assert db_config.engine == "postgresql+psycopg"
     assert db_config.username == username
     assert db_config.password == password
     assert db_config.host == host

+ 38 - 14
tests/units/test_health_endpoint.py

@@ -15,11 +15,11 @@ from reflex.utils.prerequisites import get_redis_status
     "mock_redis_client, expected_status",
     [
         # Case 1: Redis client is available and responds to ping
-        (Mock(ping=lambda: None), True),
+        (Mock(ping=lambda: None), {"redis": True}),
         # Case 2: Redis client raises RedisError
-        (Mock(ping=lambda: (_ for _ in ()).throw(RedisError)), False),
+        (Mock(ping=lambda: (_ for _ in ()).throw(RedisError)), {"redis": False}),
         # Case 3: Redis client is not used
-        (None, None),
+        (None, {"redis": None}),
     ],
 )
 async def test_get_redis_status(mock_redis_client, expected_status, mocker):
@@ -41,12 +41,12 @@ async def test_get_redis_status(mock_redis_client, expected_status, mocker):
     "mock_engine, execute_side_effect, expected_status",
     [
         # Case 1: Database is accessible
-        (MagicMock(), None, True),
+        (MagicMock(), None, {"db": True}),
         # Case 2: Database connection error (OperationalError)
         (
             MagicMock(),
             sqlalchemy.exc.OperationalError("error", "error", "error"),
-            False,
+            {"db": False},
         ),
     ],
 )
@@ -74,25 +74,49 @@ async def test_get_db_status(mock_engine, execute_side_effect, expected_status,
 
 @pytest.mark.asyncio
 @pytest.mark.parametrize(
-    "db_status, redis_status, expected_status, expected_code",
+    "db_enabled, redis_enabled, db_status, redis_status, expected_status, expected_code",
     [
         # Case 1: Both services are connected
-        (True, True, {"status": True, "db": True, "redis": True}, 200),
+        (True, True, True, True, {"status": True, "db": True, "redis": True}, 200),
         # Case 2: Database not connected, Redis connected
-        (False, True, {"status": False, "db": False, "redis": True}, 503),
+        (True, True, False, True, {"status": False, "db": False, "redis": True}, 503),
         # Case 3: Database connected, Redis not connected
-        (True, False, {"status": False, "db": True, "redis": False}, 503),
+        (True, True, True, False, {"status": False, "db": True, "redis": False}, 503),
         # Case 4: Both services not connected
-        (False, False, {"status": False, "db": False, "redis": False}, 503),
+        (True, True, False, False, {"status": False, "db": False, "redis": False}, 503),
         # Case 5: Database Connected, Redis not used
-        (True, None, {"status": True, "db": True, "redis": False}, 200),
+        (True, False, True, None, {"status": True, "db": True}, 200),
+        # Case 6: Database not used, Redis Connected
+        (False, True, None, True, {"status": True, "redis": True}, 200),
+        # Case 7: Both services not used
+        (False, False, None, None, {"status": True}, 200),
     ],
 )
-async def test_health(db_status, redis_status, expected_status, expected_code, mocker):
+async def test_health(
+    db_enabled,
+    redis_enabled,
+    db_status,
+    redis_status,
+    expected_status,
+    expected_code,
+    mocker,
+):
     # Mock get_db_status and get_redis_status
-    mocker.patch("reflex.app.get_db_status", return_value=db_status)
     mocker.patch(
-        "reflex.utils.prerequisites.get_redis_status", return_value=redis_status
+        "reflex.utils.prerequisites.check_db_used",
+        return_value=db_enabled,
+    )
+    mocker.patch(
+        "reflex.utils.prerequisites.check_redis_used",
+        return_value=redis_enabled,
+    )
+    mocker.patch(
+        "reflex.app.get_db_status",
+        return_value={"db": db_status},
+    )
+    mocker.patch(
+        "reflex.utils.prerequisites.get_redis_status",
+        return_value={"redis": redis_status},
     )
 
     # Call the async health function

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác