Prechádzať zdrojové kódy

Merge branch 'main' into remix-over-next

Khaleel Al-Adhami 3 týždňov pred
rodič
commit
c6874c1934
100 zmenil súbory, kde vykonal 3087 pridanie a 1153 odobranie
  1. 0 40
      .coveragerc
  2. 0 36
      .github/actions/setup_build_env/action.yml
  3. 6 16
      .github/workflows/benchmarks.yml
  4. 1 2
      .github/workflows/check_node_latest.yml
  5. 2 2
      .github/workflows/check_outdated_dependencies.yml
  6. 1 1
      .github/workflows/integration_app_harness.yml
  7. 5 17
      .github/workflows/integration_tests.yml
  8. 1 1
      .github/workflows/performance.yml
  9. 1 1
      .github/workflows/pre-commit.yml
  10. 2 14
      .github/workflows/unit_tests.yml
  11. 3 3
      .pre-commit-config.yaml
  12. 1 0
      .python-version
  13. 2 3
      CONTRIBUTING.md
  14. 3 3
      SECURITY.md
  15. 38 39
      pyi_hashes.json
  16. 49 10
      pyproject.toml
  17. 1 1
      reflex/.templates/jinja/web/pages/_app.js.jinja2
  18. 1 1
      reflex/.templates/jinja/web/pages/_document.js.jinja2
  19. 1 1
      reflex/.templates/jinja/web/pages/index.js.jinja2
  20. 1 1
      reflex/.templates/jinja/web/pages/stateful_component.js.jinja2
  21. 20 69
      reflex/.templates/jinja/web/pages/utils.js.jinja2
  22. 65 31
      reflex/.templates/jinja/web/tailwind.config.js.jinja2
  23. 1 1
      reflex/.templates/jinja/web/utils/context.js.jinja2
  24. 2 3
      reflex/.templates/web/components/shiki/code.js
  25. 37 17
      reflex/.templates/web/utils/state.js
  26. 1 1
      reflex/admin.py
  27. 204 103
      reflex/app.py
  28. 3 3
      reflex/app_mixins/lifespan.py
  29. 2 2
      reflex/base.py
  30. 53 14
      reflex/compiler/compiler.py
  31. 6 5
      reflex/compiler/utils.py
  32. 10 7
      reflex/components/base/bare.py
  33. 2 4
      reflex/components/base/body.py
  34. 1 1
      reflex/components/base/error_boundary.py
  35. 3 3
      reflex/components/base/link.py
  36. 5 9
      reflex/components/base/meta.py
  37. 96 116
      reflex/components/component.py
  38. 1 1
      reflex/components/core/breakpoints.py
  39. 1 1
      reflex/components/core/clipboard.py
  40. 35 4
      reflex/components/core/colors.py
  41. 2 2
      reflex/components/core/cond.py
  42. 3 3
      reflex/components/core/debounce.py
  43. 2 1
      reflex/components/core/foreach.py
  44. 2 2
      reflex/components/core/match.py
  45. 51 14
      reflex/components/core/upload.py
  46. 3 2
      reflex/components/datadisplay/dataeditor.py
  47. 2 2
      reflex/components/datadisplay/shiki_code_block.py
  48. 13 3
      reflex/components/dynamic.py
  49. 2 2
      reflex/components/el/element.py
  50. 12 8
      reflex/components/el/elements/forms.py
  51. 3 1
      reflex/components/el/elements/inline.py
  52. 3 1
      reflex/components/el/elements/typography.py
  53. 3 2
      reflex/components/gridjs/datatable.py
  54. 46 3
      reflex/components/lucide/icon.py
  55. 2 22
      reflex/components/markdown/markdown.py
  56. 1 1
      reflex/components/moment/moment.py
  57. 8 1
      reflex/components/next/video.py
  58. 3 3
      reflex/components/plotly/plotly.py
  59. 3 2
      reflex/components/radix/primitives/accordion.py
  60. 3 2
      reflex/components/radix/primitives/drawer.py
  61. 1 1
      reflex/components/radix/primitives/form.py
  62. 1 1
      reflex/components/radix/primitives/progress.py
  63. 3 2
      reflex/components/radix/primitives/slider.py
  64. 1 1
      reflex/components/radix/themes/color_mode.py
  65. 2 1
      reflex/components/radix/themes/components/checkbox_group.py
  66. 2 1
      reflex/components/radix/themes/components/radio_group.py
  67. 2 1
      reflex/components/radix/themes/components/segmented_control.py
  68. 2 1
      reflex/components/radix/themes/components/select.py
  69. 2 1
      reflex/components/radix/themes/components/slider.py
  70. 2 2
      reflex/components/radix/themes/components/tooltip.py
  71. 5 3
      reflex/components/radix/themes/layout/list.py
  72. 21 7
      reflex/components/react_player/react_player.py
  73. 7 6
      reflex/components/recharts/cartesian.py
  74. 2 1
      reflex/components/recharts/charts.py
  75. 3 2
      reflex/components/recharts/general.py
  76. 9 8
      reflex/components/recharts/polar.py
  77. 2 2
      reflex/components/recharts/recharts.py
  78. 1 1
      reflex/components/sonner/toast.py
  79. 23 25
      reflex/components/suneditor/editor.py
  80. 3 3
      reflex/components/tags/cond_tag.py
  81. 2 1
      reflex/components/tags/iter_tag.py
  82. 3 2
      reflex/components/tags/tag.py
  83. 34 24
      reflex/config.py
  84. 2 0
      reflex/constants/__init__.py
  85. 22 1
      reflex/constants/base.py
  86. 23 6
      reflex/constants/colors.py
  87. 7 0
      reflex/constants/config.py
  88. 20 6
      reflex/constants/installer.py
  89. 2 3
      reflex/constants/route.py
  90. 4 3
      reflex/constants/utils.py
  91. 67 64
      reflex/custom_components/custom_components.py
  92. 82 28
      reflex/event.py
  93. 3 2
      reflex/experimental/client_state.py
  94. 1 1
      reflex/istate/data.py
  95. 858 0
      reflex/istate/manager.py
  96. 726 2
      reflex/istate/proxy.py
  97. 2 2
      reflex/istate/storage.py
  98. 3 3
      reflex/model.py
  99. 11 2
      reflex/page.py
  100. 287 269
      reflex/reflex.py

+ 0 - 40
.coveragerc

@@ -1,40 +0,0 @@
-[run]
-source = reflex
-branch = true
-omit =
-    */pyi_generator.py
-    reflex/__main__.py
-    reflex/app_module_for_backend.py
-    reflex/components/chakra/*
-    reflex/experimental/*
-
-[report]
-show_missing = true
-# TODO bump back to 79
-fail_under = 70
-precision = 2
-
-# Regexes for lines to exclude from consideration
-exclude_also =
-    # Don't complain about missing debug-only code:
-    def __repr__
-    if self\.debug
-
-    # Don't complain if tests don't hit defensive assertion code:
-    raise AssertionError
-    raise NotImplementedError
-
-    # Don't complain if non-runnable code isn't run:
-    if 0:
-    if __name__ == .__main__.:
-
-    # Don't complain about abstract methods, they aren't run:
-    @(abc\.)?abstractmethod
-    
-    # Don't complain about overloaded methods:
-    @overload
-
-ignore_errors = True
-
-[html]
-directory = coverage_html_report

+ 0 - 36
.github/actions/setup_build_env/action.yml

@@ -6,7 +6,6 @@
 #
 # Exit conditions:
 # - Python of version `python-version` is ready to be invoked as `python`.
-# - Uv of version `uv-version` is ready to be invoked as `uv`.
 # - If `run-uv-sync` 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"
@@ -15,10 +14,6 @@ inputs:
   python-version:
     description: "Python version setup"
     required: true
-  uv-version:
-    description: "Uv version to install"
-    required: false
-    default: "0.6.9"
   run-uv-sync:
     description: "Whether to run uv sync on current dir"
     required: false
@@ -34,38 +29,7 @@ runs:
     - name: Install UV
       uses: astral-sh/setup-uv@v5
       with:
-        version: ${{ inputs.uv-version }}
         python-version: ${{ inputs.python-version }}
         enable-cache: true
         prune-cache: false
         cache-dependency-glob: "uv.lock"
-
-    - name: Restore cached project python deps
-      id: restore-pydeps-cache
-      uses: actions/cache/restore@v4
-      with:
-        path: ${{ inputs.create-venv-at-path }}
-        key: ${{ runner.os }}-python-${{ inputs.python-version }}-pydeps-${{ hashFiles('**/uv.lock') }}
-
-    - if: ${{ inputs.run-uv-sync == 'true' && steps.restore-pydeps-cache.outputs.cache-hit != 'true' }}
-      name: Run uv sync (will get cached)
-      # We skip over installing the root package (the current project code under CI)
-      # Root package should not be cached - its content is not reflected in uv.lock / cache key
-      shell: bash
-      run: |
-        uv sync --all-extras --dev --no-install-project
-
-    - if: steps.restore-pydeps-cache.outputs.cache-hit != 'true'
-      name: Save Python deps to cache
-      uses: actions/cache/save@v4
-      with:
-        path: ${{ inputs.create-venv-at-path }}
-        key: ${{ steps.restore-pydeps-cache.outputs.cache-primary-key }}
-
-    - if: ${{ inputs.run-uv-sync == 'true' }}
-      name: Run uv sync (root package)
-      # Here we really install the root package (the current project code under CI).env:
-      # This should not be cached.
-      shell: bash
-      run: |
-        uv sync --all-extras --dev

+ 6 - 16
.github/workflows/benchmarks.yml

@@ -25,22 +25,13 @@ jobs:
     #    if: github.event.pull_request.merged == true
     strategy:
       fail-fast: false
-      matrix:
-        # Show OS combos first in GUI
-        os: [ubuntu-latest]
-        python-version: ["3.12.8"]
-        node-version: ["18.x"]
 
-    runs-on: ${{ matrix.os }}
+    runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
-      - name: Use Node.js ${{ matrix.node-version }}
-        uses: actions/setup-node@v4
-        with:
-          node-version: ${{ matrix.node-version }}
       - uses: ./.github/actions/setup_build_env
         with:
-          python-version: ${{ matrix.python-version }}
+          python-version: 3.13
           run-uv-sync: true
 
       - name: Clone Reflex Website Repo
@@ -80,7 +71,7 @@ jobs:
       - uses: actions/checkout@v4
       - uses: ./.github/actions/setup_build_env
         with:
-          python-version: 3.12.8
+          python-version: 3.13
           run-uv-sync: true
 
       - name: Build reflex
@@ -90,7 +81,7 @@ jobs:
         # Only run if the database creds are available in this context.
         run:
           uv run python benchmarks/benchmark_package_size.py --os ubuntu-latest
-          --python-version 3.12.8 --commit-sha "${{ github.sha }}" --pr-id "${{ github.event.pull_request.id }}"
+          --python-version 3.13 --commit-sha "${{ github.sha }}" --pr-id "${{ github.event.pull_request.id }}"
           --branch-name "${{ github.head_ref || github.ref_name }}"
           --path ./dist
 
@@ -103,7 +94,6 @@ jobs:
       matrix:
         # Show OS combos first in GUI
         os: [ubuntu-latest, windows-latest, macos-latest]
-        python-version: ["3.12.8"]
 
     runs-on: ${{ matrix.os }}
     steps:
@@ -112,7 +102,7 @@ jobs:
         id: setup-python
         uses: actions/setup-python@v5
         with:
-          python-version: ${{ matrix.python-version }}
+          python-version: 3.13
       - name: Install UV
         uses: astral-sh/setup-uv@v5
         with:
@@ -126,7 +116,7 @@ jobs:
       - name: calculate and upload size
         run:
           uv run python benchmarks/benchmark_package_size.py --os "${{ matrix.os }}"
-          --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}"
+          --python-version "3.13" --commit-sha "${{ github.sha }}"
           --pr-id "${{ github.event.pull_request.id }}"
           --branch-name "${{ github.head_ref || github.ref_name }}"
           --path ./.venv

+ 1 - 2
.github/workflows/check_node_latest.yml

@@ -18,7 +18,6 @@ jobs:
     runs-on: ubuntu-22.04
     strategy:
       matrix:
-        python-version: ["3.12.8"]
         split_index: [1, 2]
         node-version: ["node"]
       fail-fast: false
@@ -27,7 +26,7 @@ jobs:
       - uses: actions/checkout@v4
       - uses: ./.github/actions/setup_build_env
         with:
-          python-version: ${{ matrix.python-version }}
+          python-version: 3.13
           run-uv-sync: true
 
       - uses: actions/setup-node@v4

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

@@ -18,7 +18,7 @@ jobs:
 
       - uses: ./.github/actions/setup_build_env
         with:
-          python-version: "3.10"
+          python-version: 3.13
           run-uv-sync: true
 
       - name: Check outdated backend dependencies
@@ -45,7 +45,7 @@ jobs:
         uses: actions/checkout@v4
       - uses: ./.github/actions/setup_build_env
         with:
-          python-version: "3.10.16"
+          python-version: 3.13
           run-uv-sync: true
 
       - name: Clone Reflex Website Repo

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

@@ -25,7 +25,7 @@ jobs:
     strategy:
       matrix:
         state_manager: ["redis", "memory"]
-        python-version: ["3.11.11", "3.12.8", "3.13.1"]
+        python-version: ["3.11", "3.12", "3.13"]
         split_index: [1, 2]
       fail-fast: false
     runs-on: ubuntu-22.04

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

@@ -43,17 +43,7 @@ jobs:
       matrix:
         # Show OS combos first in GUI
         os: [ubuntu-latest, windows-latest]
-        python-version: ["3.10.16", "3.11.11", "3.12.8", "3.13.1"]
-        exclude:
-          - os: windows-latest
-            python-version: "3.11.11"
-          - os: windows-latest
-            python-version: "3.10.16"
-        include:
-          - os: windows-latest
-            python-version: "3.11.9"
-          - os: windows-latest
-            python-version: "3.10.11"
+        python-version: ["3.10", "3.11", "3.12", "3.13"]
 
     runs-on: ${{ matrix.os }}
     steps:
@@ -114,13 +104,11 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        # Show OS combos first in GUI
-        os: [ubuntu-latest]
-        python-version: ["3.11.11", "3.12.8"]
+        python-version: ["3.11", "3.12"]
 
     env:
       REFLEX_WEB_WINDOWS_OVERRIDE: "1"
-    runs-on: ${{ matrix.os }}
+    runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
       - uses: ./.github/actions/setup_build_env
@@ -155,7 +143,7 @@ jobs:
       - uses: actions/checkout@v4
       - uses: ./.github/actions/setup_build_env
         with:
-          python-version: "3.11.11"
+          python-version: 3.13
           run-uv-sync: true
 
       - name: Create app directory
@@ -181,7 +169,7 @@ jobs:
       fail-fast: false
       matrix:
         # Note: py311 version chosen due to available arm64 darwin builds.
-        python-version: ["3.11.9", "3.12.8"]
+        python-version: ["3.11", "3.12"]
     runs-on: macos-latest
     steps:
       - uses: actions/checkout@v4

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

@@ -31,7 +31,7 @@ jobs:
       - name: Set up Python
         uses: actions/setup-python@v5
         with:
-          python-version: "3.12.8"
+          python-version: "3.13"
 
       - name: Install dependencies
         run: uv sync --all-extras --dev

+ 1 - 1
.github/workflows/pre-commit.yml

@@ -23,7 +23,7 @@ jobs:
       - uses: actions/checkout@v4
       - uses: ./.github/actions/setup_build_env
         with:
-          python-version: 3.13.2
+          python-version: 3.13
           run-uv-sync: true
       - uses: actions/checkout@v4
         with:

+ 2 - 14
.github/workflows/unit_tests.yml

@@ -28,18 +28,7 @@ jobs:
       fail-fast: false
       matrix:
         os: [ubuntu-latest, windows-latest]
-        python-version: ["3.10.16", "3.11.11", "3.12.8", "3.13.1"]
-        # Windows is a bit behind on Python version availability in Github
-        exclude:
-          - os: windows-latest
-            python-version: "3.11.11"
-          - os: windows-latest
-            python-version: "3.10.16"
-        include:
-          - os: windows-latest
-            python-version: "3.11.9"
-          - os: windows-latest
-            python-version: "3.10.11"
+        python-version: ["3.10", "3.11", "3.12", "3.13"]
     runs-on: ${{ matrix.os }}
 
     # Service containers to run with `runner-job`
@@ -88,8 +77,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        # Note: py310, py311 versions chosen due to available arm64 darwin builds.
-        python-version: ["3.10.11", "3.11.9", "3.12.8", "3.13.1"]
+        python-version: ["3.10", "3.11", "3.12", "3.13"]
     runs-on: macos-latest
     steps:
       - uses: actions/checkout@v4

+ 3 - 3
.pre-commit-config.yaml

@@ -1,8 +1,8 @@
 fail_fast: true
 
 repos:
-  - repo: https://github.com/charliermarsh/ruff-pre-commit
-    rev: v0.11.2
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    rev: v0.11.8
     hooks:
       - id: ruff-format
         args: [reflex, tests]
@@ -30,7 +30,7 @@ repos:
         entry: python3 scripts/make_pyi.py
 
   - repo: https://github.com/RobertCraigie/pyright-python
-    rev: v1.1.398
+    rev: v1.1.400
     hooks:
       - id: pyright
         args: [reflex, tests]

+ 1 - 0
.python-version

@@ -0,0 +1 @@
+3.13

+ 2 - 3
CONTRIBUTING.md

@@ -23,7 +23,7 @@ cd reflex
 **3. Install your local Reflex build:**
 
 ```bash
-uv sync --python 3.13
+uv sync
 ```
 
 **4. Now create an examples folder so you can test the local Python build in this repository.**
@@ -86,10 +86,9 @@ uv run ruff format .
 ```
 
 Consider installing git pre-commit hooks so Ruff, Pyright, Darglint and `make_pyi` will run automatically before each commit.
-Note that pre-commit will only be installed when you use a Python version >= 3.10.
 
 ```bash
-pre-commit install
+uv run pre-commit install
 ```
 
 That's it you can now submit your PR. Thanks for contributing to Reflex!

+ 3 - 3
SECURITY.md

@@ -2,9 +2,9 @@
 
 ## Supported Versions
 
-| Version   | Supported          |
-| --------- | ------------------ |
-| >= 0.1.18 | :white_check_mark: |
+| Version  | Supported          |
+| -------- | ------------------ |
+| >= 0.7.0 | :white_check_mark: |
 
 ## Reporting a Vulnerability
 

+ 38 - 39
pyi_hashes.json

@@ -3,62 +3,61 @@
   "reflex/components/__init__.pyi": "76ba0a12cd3a7ba5ab6341a3ae81551f",
   "reflex/components/base/__init__.pyi": "d0139bd2c41c28d837b91fa8949e2494",
   "reflex/components/base/app_wrap.pyi": "387fc7a0c2da8760d9449e2893e44eec",
-  "reflex/components/base/body.pyi": "2cc870cec4b1c28081dd40467752c2b7",
-  "reflex/components/base/document.pyi": "851cb54842f3df1e5f44ba1fb72e12af",
+  "reflex/components/base/body.pyi": "2d16002f24c8ee0007b46ff2bf1f2c78",
+  "reflex/components/base/document.pyi": "30377cdfb02b564f8de29b0473d2346c",
   "reflex/components/base/error_boundary.pyi": "c56b591d14a92b99a1e97e04afe167d7",
   "reflex/components/base/fragment.pyi": "603ee8e03af88d4a8ff6bc1fbce4e022",
   "reflex/components/base/head.pyi": "893047aa32da553711db8f1345adb6b0",
-  "reflex/components/base/link.pyi": "396488afa3b7a5b0d0e6c5e89159f857",
-  "reflex/components/base/meta.pyi": "bc4b4fda6f022a517de339ffdd667e3b",
-  "reflex/components/base/script.pyi": "d3743541fd968e9b6aaa123da6a11c3c",
+  "reflex/components/base/link.pyi": "e96179dc7823f354fb73a6c03e31028c",
+  "reflex/components/base/meta.pyi": "da52c3212fac6b50560863146a7afcc3",
+  "reflex/components/base/script.pyi": "530cf8f47eb90082bf65942e8b5d745f",
   "reflex/components/base/strict_mode.pyi": "d972e0ff2a6f961e7df90fc27b8bb51b",
   "reflex/components/core/__init__.pyi": "d99fbfd4207d8a3f7013221f428e0ed8",
   "reflex/components/core/auto_scroll.pyi": "d3012d2a4ccaab8dfebf9aa484020f59",
   "reflex/components/core/banner.pyi": "48d0eb86ae09e806ebe20d0edcc3cdb3",
-  "reflex/components/core/client_side_routing.pyi": "919944e006257c6ee7eecd772e476dc6",
-  "reflex/components/core/clipboard.pyi": "af76b623d593df3b16162033c597f920",
-  "reflex/components/core/debounce.pyi": "76d857eb814bc64625860a5f43e8b230",
-  "reflex/components/core/helmet.pyi": "835ea00c34746b334d33af448dce7422",
+  "reflex/components/core/client_side_routing.pyi": "9be638a2b0e00b8181697e5dd6b45e4e",
+  "reflex/components/core/clipboard.pyi": "4098368af3c32dbde77fc74599f8799a",
+  "reflex/components/core/debounce.pyi": "affda049624c266c7d5620efa3b7041b",
   "reflex/components/core/html.pyi": "b12117b42ef79ee90b6b4dec50baeb86",
   "reflex/components/core/sticky.pyi": "c65131cf7c2312c68e1fddaa0cc27150",
-  "reflex/components/core/upload.pyi": "6fb89607ec8f8c1971f0dbd3453901eb",
+  "reflex/components/core/upload.pyi": "4680da6f7b3df704a682cc6441b1ac18",
   "reflex/components/datadisplay/__init__.pyi": "cf087efa8b3960decc6b231cc986cfa9",
   "reflex/components/datadisplay/code.pyi": "3d8f0ab4c2f123d7f80d15c7ebc553d9",
-  "reflex/components/datadisplay/dataeditor.pyi": "1b762071001161e4fdd1285263c33bb3",
+  "reflex/components/datadisplay/dataeditor.pyi": "cb03d732e2fe771a8d46c7bcda671f92",
   "reflex/components/datadisplay/shiki_code_block.pyi": "87db7639bfa5cd53e1709e1363f93278",
   "reflex/components/el/__init__.pyi": "09042a2db5e0637e99b5173430600522",
-  "reflex/components/el/element.pyi": "73bdb7272e6e800562e6afe390b6dfc0",
+  "reflex/components/el/element.pyi": "ea6b33a8545c2c845dc6c30ff1c872a4",
   "reflex/components/el/elements/__init__.pyi": "280ed457675f3720e34b560a3f617739",
   "reflex/components/el/elements/base.pyi": "6e533348b5e1a88cf62fbb5a38dbd795",
-  "reflex/components/el/elements/forms.pyi": "2e7ab39bc7295b8594f38a2aa59c9610",
-  "reflex/components/el/elements/inline.pyi": "33d9d860e75dd8c4769825127ed363bb",
+  "reflex/components/el/elements/forms.pyi": "3ff8fd5d8a36418874e9fe4ff76bbfe8",
+  "reflex/components/el/elements/inline.pyi": "f881d229c9ecaa61d17ac6837ac9a839",
   "reflex/components/el/elements/media.pyi": "addd6872281d65d44a484358b895432f",
   "reflex/components/el/elements/metadata.pyi": "a5b9b30c4649e88aa26a1a5609fc86ef",
   "reflex/components/el/elements/other.pyi": "995a4fbf10bfdb7f48808210dfe413bd",
   "reflex/components/el/elements/scripts.pyi": "cd5bd53c3a6b016fbb913aff36d63344",
   "reflex/components/el/elements/sectioning.pyi": "65aa53b1372598ec1785616cb7016032",
   "reflex/components/el/elements/tables.pyi": "e1282d8ddf4efa4c911ca104a907ee88",
-  "reflex/components/el/elements/typography.pyi": "00088c9c1b68a14e5a41d837e8fdf542",
-  "reflex/components/gridjs/datatable.pyi": "7fd1dd65ba143d60b7d42d1bb90a179d",
-  "reflex/components/lucide/icon.pyi": "a5521a8baf8d2d7281e3fdfe6ce7073b",
-  "reflex/components/markdown/markdown.pyi": "70c84b8340bf2bc94c2230f1d319ba27",
+  "reflex/components/el/elements/typography.pyi": "928ff998c9bbb32ae7ccce5f6cb885a7",
+  "reflex/components/gridjs/datatable.pyi": "3db3f994640c19be5c3fa2983f71de56",
+  "reflex/components/lucide/icon.pyi": "508c8844959925555a895df8dcac3751",
+  "reflex/components/markdown/markdown.pyi": "1fc31d2652d3ff015c6da2c7cbab716a",
   "reflex/components/moment/moment.pyi": "6dd0c7cee5f0f29bc11d830c697d7f92",
   "reflex/components/next/base.pyi": "14aafd5b018a4bc9748a3c9980fcfe3e",
   "reflex/components/next/image.pyi": "3a0d1970e69144e9c6806e68ab99f181",
   "reflex/components/next/link.pyi": "cd913e10205314afe67101d9640e05cb",
-  "reflex/components/next/video.pyi": "09698418db651917630a7fefeb573fc2",
-  "reflex/components/plotly/plotly.pyi": "77afe88b405c3eae7058994d53a27946",
+  "reflex/components/next/video.pyi": "aa8f814dec99f8712dc2351b15f922ac",
+  "reflex/components/plotly/plotly.pyi": "b1f0bbcaf4706d0a373c99395ba50118",
   "reflex/components/radix/__init__.pyi": "8d586cbff1d7130d09476ac72ee73400",
   "reflex/components/radix/primitives/__init__.pyi": "fe8715decf3e9ae471b56bba14e42cb3",
-  "reflex/components/radix/primitives/accordion.pyi": "b1482766c3c99ab40c2f446598fdb6a7",
+  "reflex/components/radix/primitives/accordion.pyi": "54427d58c5e1498ad2c6189214bba28a",
   "reflex/components/radix/primitives/base.pyi": "8b1dbf0b75cb29e873d611b83c9e4156",
-  "reflex/components/radix/primitives/drawer.pyi": "b6f8b17e1d0064d5609915546c722a81",
+  "reflex/components/radix/primitives/drawer.pyi": "95cc7c2fdc5407f8ceca06199d4503fa",
   "reflex/components/radix/primitives/form.pyi": "79ddb679e0b3df814439ce993fcf355e",
   "reflex/components/radix/primitives/progress.pyi": "c62a0c44e0d440701174fcca93bf8fbe",
-  "reflex/components/radix/primitives/slider.pyi": "c27e1a1180442e2e6e9d727560e8068c",
+  "reflex/components/radix/primitives/slider.pyi": "10196fb967c9cde3860a930a526b6c51",
   "reflex/components/radix/themes/__init__.pyi": "a15f9464ad99f248249ffa8e6deea4cf",
   "reflex/components/radix/themes/base.pyi": "a3c3c3b72fd3d8f1e38990e5c461b682",
-  "reflex/components/radix/themes/color_mode.pyi": "435a51382eab6111aae1b26e79e9a473",
+  "reflex/components/radix/themes/color_mode.pyi": "e18fe42952d10f5733f3baf4789c4bb5",
   "reflex/components/radix/themes/components/__init__.pyi": "87bb9ffff641928562da1622d2ca5993",
   "reflex/components/radix/themes/components/alert_dialog.pyi": "8e1dde62450296310a116ed066bd51e3",
   "reflex/components/radix/themes/components/aspect_ratio.pyi": "1845813a034adfc1f5db8e0f6ffc1118",
@@ -69,7 +68,7 @@
   "reflex/components/radix/themes/components/card.pyi": "fe42e0cbdf9eb34341f4bbba8a586b34",
   "reflex/components/radix/themes/components/checkbox.pyi": "78bc26eabd6468a44f5139449a2c6208",
   "reflex/components/radix/themes/components/checkbox_cards.pyi": "cc43c568aa42ffa3e693e5cd1acba156",
-  "reflex/components/radix/themes/components/checkbox_group.pyi": "e36603b9ea5f161070c5a0235c4411fa",
+  "reflex/components/radix/themes/components/checkbox_group.pyi": "b798c7cca10f4493484dc1621c0eed9c",
   "reflex/components/radix/themes/components/context_menu.pyi": "cdf546723a84c99412d91ca63d4bb2df",
   "reflex/components/radix/themes/components/data_list.pyi": "768e4c9222d37d90228309166a1c6ab3",
   "reflex/components/radix/themes/components/dialog.pyi": "b51cb34dc6c90ccd07a2f9fc97eaf1c1",
@@ -81,20 +80,20 @@
   "reflex/components/radix/themes/components/progress.pyi": "c880c6bb9803d47048f656dfa66a7c15",
   "reflex/components/radix/themes/components/radio.pyi": "36fa5585440685a7d2dff40b50502840",
   "reflex/components/radix/themes/components/radio_cards.pyi": "e9a0f27119322e6148946ae178edb7a9",
-  "reflex/components/radix/themes/components/radio_group.pyi": "ea3180940390e4b6eaf10670be5bc2fe",
+  "reflex/components/radix/themes/components/radio_group.pyi": "510e2ac6aebec248c275f4ddb25940a9",
   "reflex/components/radix/themes/components/scroll_area.pyi": "83892be0b2c902d2147cbdb5e19310ab",
-  "reflex/components/radix/themes/components/segmented_control.pyi": "7be200991becc54cd885465656e2dfef",
-  "reflex/components/radix/themes/components/select.pyi": "655a5c2182a16121440e5ddbba2079d8",
+  "reflex/components/radix/themes/components/segmented_control.pyi": "ee1b8cb2cada89459d17a186206f3c3a",
+  "reflex/components/radix/themes/components/select.pyi": "869d36f7a20b466bc15c634c7c0ee0dd",
   "reflex/components/radix/themes/components/separator.pyi": "58a95aca75a556d349eb56f898bde680",
   "reflex/components/radix/themes/components/skeleton.pyi": "d91615706e5efb81d97755decbbf5ae3",
-  "reflex/components/radix/themes/components/slider.pyi": "b87ee08b7edfe41eddf3d3c1cb71124e",
+  "reflex/components/radix/themes/components/slider.pyi": "8caaea62efdd0b4b9878a63620c97632",
   "reflex/components/radix/themes/components/spinner.pyi": "80766a7324b582221edb66ec46da0acb",
   "reflex/components/radix/themes/components/switch.pyi": "f8256d2b50d15ab163649cfb05229750",
   "reflex/components/radix/themes/components/table.pyi": "560ce8d920e03b450fe6b938f5f0fea0",
   "reflex/components/radix/themes/components/tabs.pyi": "96ac1082651d2adc2a60a3af6e90c17f",
   "reflex/components/radix/themes/components/text_area.pyi": "418d3df53eeca0723d83a93d81f16b12",
   "reflex/components/radix/themes/components/text_field.pyi": "cdf0e08f5af0a5fce6b31787001f1dc3",
-  "reflex/components/radix/themes/components/tooltip.pyi": "6cd225ba10140e925752c74404336f27",
+  "reflex/components/radix/themes/components/tooltip.pyi": "c37fb988ec52da25be83083c3a85524a",
   "reflex/components/radix/themes/layout/__init__.pyi": "9a52c5b283c864be70b51a8fd6120392",
   "reflex/components/radix/themes/layout/base.pyi": "e9a5c1f376e66653ebcf5d2315f990f8",
   "reflex/components/radix/themes/layout/box.pyi": "5a3c2339d74cc062358ec32b2c2c138c",
@@ -102,7 +101,7 @@
   "reflex/components/radix/themes/layout/container.pyi": "4020c3dca660027b84d11cc4198393c4",
   "reflex/components/radix/themes/layout/flex.pyi": "f814281a5635ad43dd1df23f8e356c66",
   "reflex/components/radix/themes/layout/grid.pyi": "6062188367a2c253f014f916197c963d",
-  "reflex/components/radix/themes/layout/list.pyi": "804f7a36c103cd7a3e362d40a58e8d39",
+  "reflex/components/radix/themes/layout/list.pyi": "930009f82662686841e9ce97bfd4a1ea",
   "reflex/components/radix/themes/layout/section.pyi": "41895910072e023ed0fef6a8ad956046",
   "reflex/components/radix/themes/layout/spacer.pyi": "029eb0eaa731bcdff7c496e0437e22b1",
   "reflex/components/radix/themes/layout/stack.pyi": "3b0da99b00c826d087ed89fc67c595c1",
@@ -112,16 +111,16 @@
   "reflex/components/radix/themes/typography/heading.pyi": "5a3b0b8e44bda0fce22c6b1a1f25e68e",
   "reflex/components/radix/themes/typography/link.pyi": "b86d4e406db6cdd42daf83c7e1d91e0d",
   "reflex/components/radix/themes/typography/text.pyi": "e6aa0ca43ebbd42701a3c72c0312032e",
-  "reflex/components/react_player/audio.pyi": "972975ed0ba3e1dc4a867da20b11ae8e",
-  "reflex/components/react_player/react_player.pyi": "63ffffbc24907103f797dcfd85894107",
-  "reflex/components/react_player/video.pyi": "35ce5ad62e8bff17d9c09d27c362f8dc",
+  "reflex/components/react_player/audio.pyi": "18fb682ec86d1b44682e1903dff11794",
+  "reflex/components/react_player/react_player.pyi": "171d829b30c1c0c62e49e4a21cffe50f",
+  "reflex/components/react_player/video.pyi": "5c93cfe85ba4dcadfddae94a2e36bb4e",
   "reflex/components/recharts/__init__.pyi": "a52c9055e37c6ee25ded15688d45e8a5",
-  "reflex/components/recharts/cartesian.pyi": "34b15e8f5125b5a8145e3e04ed6418e4",
-  "reflex/components/recharts/charts.pyi": "b3d35de9cea86307ad2ab7d69ff2d06b",
-  "reflex/components/recharts/general.pyi": "5548fc494c29063c262ca7a7ef51dce8",
-  "reflex/components/recharts/polar.pyi": "8fb87fd69c9edf55998f11ea8ada76fb",
+  "reflex/components/recharts/cartesian.pyi": "9dd16c08abe5205c6c414474e2de2f79",
+  "reflex/components/recharts/charts.pyi": "3570af4627c601d10ee37033f1b2329c",
+  "reflex/components/recharts/general.pyi": "a1b846d5f2fd0a8b1969b472c5cab2e7",
+  "reflex/components/recharts/polar.pyi": "973c3e6aa253914c4c5fd18ed32196fb",
   "reflex/components/recharts/recharts.pyi": "157acc830323075ffaf4f68d495d1787",
   "reflex/components/sonner/toast.pyi": "0b6dc33413f30fdd043b89ec3c8c3f39",
-  "reflex/components/suneditor/editor.pyi": "284aa914b9bffe840db67ee68192eaf7",
+  "reflex/components/suneditor/editor.pyi": "7d94c3587f9ee15e4ab68aca8c3a6d8b",
   "reflex/experimental/layout.pyi": "6398e779743963ef3e03396696b8ddfb"
 }

+ 49 - 10
pyproject.toml

@@ -1,6 +1,6 @@
 [project]
 name = "reflex"
-version = "0.7.7dev1"
+version = "0.7.11dev1"
 description = "Web apps in pure Python."
 license = { text = "Apache-2.0" }
 authors = [
@@ -21,25 +21,22 @@ keywords = ["web", "framework"]
 requires-python = ">=3.10,<4.0"
 dependencies = [
   "alembic >=1.15.2,<2.0",
-  "distro >=1.9.0,<2.0; platform_system == 'Linux'",
   "fastapi >=0.115.0",
-  "granian[reload] >=2.2.3",
-  "gunicorn >=23.0.0,<24.0.0",
+  "granian[reload] >=2.2.5",
   "httpx >=0.28.0,<1.0",
   "jinja2 >=3.1.2,<4.0",
-  "packaging >=24.2,<25.0",
+  "packaging >=24.2,<26",
   "platformdirs >=4.3.7,<5.0",
   "psutil >=7.0.0,<8.0",
   "pydantic >=1.10.21,<3.0",
   "python-socketio >=5.12.0,<6.0",
   "python-multipart >=0.0.20,<1.0",
   "redis >=5.2.1,<6.0",
-  "reflex-hosting-cli >=0.1.38",
-  "rich >=13.0.0,<14.0",
+  "reflex-hosting-cli >=0.1.47",
+  "rich >=13,<15",
   "sqlmodel >=0.0.24,<0.1",
-  "typer >=0.15.2,<1.0",
+  "click >=8",
   "typing_extensions >=4.13.0",
-  "uvicorn >=0.34.0",
   "wrapt >=1.17.0,<2.0",
 ]
 classifiers = [
@@ -103,6 +100,7 @@ lint.select = [
   "SIM",
   "T",
   "TRY",
+  "UP",
   "W",
 ]
 lint.ignore = [
@@ -115,6 +113,7 @@ lint.ignore = [
   "RUF008",
   "RUF012",
   "TRY0",
+  "UP038",
 ]
 lint.pydocstyle.convention = "google"
 
@@ -151,7 +150,7 @@ dev = [
   "plotly >=6.0",
   "pre-commit >=4.2",
   "psycopg[binary] >=3.2",
-  "pyright >=1.1.399",
+  "pyright >=1.1.400",
   "pytest >=8.3",
   "pytest-asyncio >=0.26",
   "pytest-benchmark >=5.1",
@@ -165,4 +164,44 @@ dev = [
   "ruff >=0.11",
   "selenium >=4.31",
   "starlette-admin >=0.14",
+  "uvicorn >=0.34.0",
+]
+
+
+[tool.coverage.run]
+source = ["reflex"]
+branch = true
+omit = [
+  "*/pyi_generator.py",
+  "reflex/__main__.py",
+  "reflex/app_module_for_backend.py",
+  "reflex/components/chakra/*",
+  "reflex/experimental/*",
 ]
+
+[tool.coverage.report]
+show_missing = true
+# TODO bump back to 79
+fail_under = 70
+precision = 2
+ignore_errors = true
+
+exclude_also = [
+  "def __repr__",
+  # Don't complain about missing debug-only code:
+  "if self.debug",
+  # Don't complain if tests don't hit defensive assertion code:
+  "raise AssertionError",
+  "raise NotImplementedError",
+  # Regexes for lines to exclude from consideration
+  "if 0:",
+  # Don't complain if non-runnable code isn't run:
+  "if __name__ == .__main__.:",
+  # Don't complain about abstract methods, they aren't run:
+  "@(abc.)?abstractmethod",
+  # Don't complain about overloaded methods:
+  "@overload",
+]
+
+[tool.coverage.html]
+directory = "coverage_html_report"

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

@@ -24,7 +24,7 @@ function AppWrap({children}) {
   {{ renderHooks(hooks) }}
 
   return (
-    {{utils.render(render, indent_width=0)}}
+    {{utils.render(render)}}
   )
 }
 

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

@@ -3,7 +3,7 @@
 {% block export %}
 export function Layout({children}) {
   return (
-    {{utils.render(document, indent_width=0)}}
+    {{utils.render(document)}}
   )
 }
 {% endblock %}

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

@@ -12,7 +12,7 @@ export default function Component() {
     {{ renderHooks(hooks)}}
 
   return (
-    {{utils.render(render, indent_width=0)}}
+    {{utils.render(render)}}
   )
 }
 {% endblock %}

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

@@ -6,6 +6,6 @@ export function {{tag_name}} () {
   {{ renderHooksWithMemo(all_hooks, memo_trigger_hooks) }}
   
   return (
-    {{utils.render(component.render(), indent_width=0)}}
+    {{utils.render(component.render())}}
   )
 }

+ 20 - 69
reflex/.templates/jinja/web/pages/utils.js.jinja2

@@ -1,40 +1,25 @@
 {# Rendering components recursively. #}
 {# Args: #}
 {#     component: component dictionary #}
-{#     indent_width: indent width #}
-{% macro render(component, indent_width=0) %}
-{% filter indent(width=indent_width) %}
-  {%- if component is not mapping %}
-    {{- component }}
-  {%- elif "iterable" in component %}
-    {{- render_iterable_tag(component) }}
-  {%- elif component.name == "match"%}
-    {{- render_match_tag(component) }}
-  {%- elif "cond" in component %}
-    {{- render_condition_tag(component) }}
-  {%- elif component.children|length %}
-    {{- render_tag(component) }}
-  {%- else %}
-    {{- render_self_close_tag(component) }}
-  {%- endif %}
-{% endfilter %}
+{% macro render(component) %}
+{%- if component is not mapping %}{{ component }}
+{%- elif "iterable" in component %}{{ render_iterable_tag(component) }}
+{%- elif component.name == "match"%}{{ render_match_tag(component) }}
+{%- elif "cond" in component %}{{ render_condition_tag(component) }}
+{%- elif component.children|length %}{{ render_tag(component) }}
+{%- else %}{{ render_self_close_tag(component) }}
+{%- endif %}
 {% endmacro %}
 
 {# Rendering self close tag. #}
 {# Args: #}
 {#     component: component dictionary #}
 {% macro render_self_close_tag(component) %}
-{%- if component.name|length -%}
-jsx(
-  {{ component.name }},
-  {{- render_props(component.props) }},
-  {{- component.contents }}
-)
-{%- elif component.contents|length -%}
-  {{- component.contents }}
-{%- else -%}
-  ""
-{%- endif -%}
+{% if component.name|length %}
+jsx({{ component.name }},{{ render_props(component.props) }},{{ component.contents }})
+{% elif component.contents|length -%}{{ component.contents }}
+{% else %}""
+{% endif %}
 {% endmacro %}
 
 {# Rendering close tag with args and props. #}
@@ -42,20 +27,10 @@ jsx(
 {#     component: component dictionary #}
 {% macro render_tag(component) %}
 jsx(
-  {% if component.name|length %}
-  {{ component.name }},
-  {%- else -%}
-  Fragment,
-  {% endif -%}
-  {{- render_props(component.props) -}},
-  {%- if component.contents|length -%}
-    {{- component.contents -}},
-  {%- endif -%}
-  {%- for child in component.children -%}
-    {%- if child is mapping or child|length %}
-      {{- render(child) -}},
-    {%- endif -%}
-  {%- endfor -%}
+{% if component.name|length %}{{ component.name }}{% else %}Fragment{% endif %},
+{{ render_props(component.props) }},
+{% if component.contents|length %}{{ component.contents }},{% endif %}
+{% for child in component.children %}{% if child is mapping or child|length %}{{ render(child) }},{% endif %}{% endfor %}
 )
 {%- endmacro %}
 
@@ -64,11 +39,7 @@ jsx(
 {# Args: #}
 {#     component: component dictionary #}
 {% macro render_condition_tag(component) %}
-{{- component.cond_state }} ? (
-  {{ render(component.true_value) }}
-) : (
-  {{ render(component.false_value) }}
-)
+({{ component.cond_state }} ? ({{ render(component.true_value) }}) : ({{ render(component.false_value) }}))
 {%- endmacro %}
 
 
@@ -76,18 +47,14 @@ jsx(
 {# Args: #}
 {#     component: component dictionary #}
 {% macro render_iterable_tag(component) %}
-{{ component.iterable_state }}.map(({{ component.arg_name }}, {{ component.arg_index }}) => (
-  {% for child in component.children %}
-  {{ render(child) }}
-  {% endfor %}
-))
+{{ component.iterable_state }}.map(({{ component.arg_name }},{{ component.arg_index }})=>({% for child in component.children %}{{ render(child) }}{% endfor %}))
 {%- endmacro %}
 
 
 {# Rendering props of a component. #}
 {# Args: #}
 {#     component: component dictionary #}
-{% macro render_props(props) %}{ {%- if props|length -%} {{- props|join(", ") -}}{%- endif -%} }{% endmacro %}
+{% macro render_props(props) %}{{ "{" }}{% if props|length %}{{ props|join(",") }}{% endif %}{{ "}" }}{% endmacro %}
 
 {# Rendering Match component. #}
 {# Args: #}
@@ -107,22 +74,6 @@ jsx(
       break;
   }
 })()
-{%- endmacro %}
-
-
-{# Rendering content with args. #}
-{# Args: #}
-{#     component: component dictionary #}
-{% macro render_arg_content(component) %}
-{% filter indent(width=2) %}
-{# no string below for a line break #}
-
-{({ {{component.args|join(", ")}} }) => (
-  {% for child in component.children %}
-  {{ render(child) }}
-  {% endfor %}
-)}
-{% endfilter %}
 {% endmacro %}
 
 

+ 65 - 31
reflex/.templates/jinja/web/tailwind.config.js.jinja2

@@ -1,32 +1,66 @@
-/** @type {import('tailwindcss').Config} */
-module.exports = {
-	content: {{content|json_dumps}},
-	theme: {{theme|json_dumps}},
-	plugins: [
-	{% for plugin in plugins %}
-		require({{plugin|json_dumps}}),
-	{% endfor %}
-	],
-	{% if presets is defined %}
-	presets: [
-	{% for preset in presets %}
-		require({{preset|json_dumps}})
-	{% endfor %}
-	],
-	{% endif %}
-	{% if darkMode is defined %}
-	darkMode: {{darkMode|json_dumps}},
-	{% endif %}
-	{% if corePlugins is defined %}
-	corePlugins: {{corePlugins|json_dumps}},
-	{% endif %}
-	{% if important is defined %}
-	important: {{important|json_dumps}},
-	{% endif %}
-	{% if prefix is defined %}
-	prefix: {{prefix|json_dumps}},
-	{% endif %}
-	{% if separator is defined %}
-	separator: {{separator|json_dumps}},
-	{% endif %}
+{# Helper macro to render JS objects and arrays #}
+{% macro render_js(val, indent=2, level=0) -%}
+{%- set space = ' ' * (indent * level) -%}
+{%- set next_space = ' ' * (indent * (level + 1)) -%}
+
+{%- if val is mapping -%}
+{
+{%- for k, v in val.items() %}
+{{ next_space }}{{ k if k is string and k.isidentifier() else k|tojson }}: {{ render_js(v, indent, level + 1) }}{{ "," if not loop.last }}
+{%- endfor %}
+{{ space }}}
+{%- elif val is iterable and val is not string -%}
+[
+{%- for item in val %}
+{{ next_space }}{{ render_js(item, indent, level + 1) }}{{ "," if not loop.last }}
+{%- endfor %}
+{{ space }}]
+{%- else -%}
+{{ val | tojson }}
+{%- endif -%}
+{%- endmacro %}
+
+{# Extract destructured imports from plugin dicts only #}
+{%- set imports = [] %}
+{%- for plugin in plugins if plugin is mapping and plugin.import is defined %}
+  {%- set _ = imports.append(plugin.import) %}
+{%- endfor %}
+
+/** @type {import('tailwindcss').Config} */
+{%- for imp in imports %}
+const { {{ imp.name }} } = require({{ imp.from | tojson }});  
+{%- endfor %}
+
+module.exports = {
+  content: {{ render_js(content) }},
+  theme: {{ render_js(theme) }},
+  {% if darkMode is defined %}darkMode: {{ darkMode | tojson }},{% endif %}
+  {% if corePlugins is defined %}corePlugins: {{ render_js(corePlugins) }},{% endif %}
+  {% if important is defined %}important: {{ important | tojson }},{% endif %}
+  {% if prefix is defined %}prefix: {{ prefix | tojson }},{% endif %}
+  {% if separator is defined %}separator: {{ separator | tojson }},{% endif %}
+  {% if presets is defined %}
+  presets: [
+    {% for preset in presets %}
+    require({{ preset | tojson }}){{ "," if not loop.last }}
+    {% endfor %}
+  ],
+  {% endif %}
+  plugins: [
+    {% for plugin in plugins %}
+      {% if plugin is mapping %}
+        {% if plugin.call is defined %}
+          {{ plugin.call }}(
+            {%- if plugin.args is defined -%}
+              {{ render_js(plugin.args) }}
+            {%- endif -%}
+          ){{ "," if not loop.last }}
+        {% else %}
+          require({{ plugin.name | tojson }}){{ "," if not loop.last }}
+        {% endif %}
+      {% else %}
+        require({{ plugin | tojson }}){{ "," if not loop.last }}
+      {% endif %}
+    {% endfor %}
+  ]
 };

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

@@ -1,4 +1,4 @@
-import { createContext, useContext, useMemo, useReducer, useState, createElement } from "react"
+import { createContext, useContext, useMemo, useReducer, useState, createElement, createElement } from "react"
 import { applyDelta, Event, hydrateClientStorage, useEventLoop, refs } from "$/utils/state"
 
 {% if initial_state %}

+ 2 - 3
reflex/.templates/web/components/shiki/code.js

@@ -1,6 +1,5 @@
-import { useEffect, useState } from "react";
+import { useEffect, useState, createElement } from "react";
 import { codeToHtml } from "shiki";
-import { jsx } from "@emotion/react";
 
 /**
  * Code component that uses Shiki to convert code to HTML and render it.
@@ -34,7 +33,7 @@ export function Code({
     }
     fetchCode();
   }, [code, language, theme, transformers, decorations]);
-  return jsx("div", {
+  return createElement("div", {
     dangerouslySetInnerHTML: { __html: codeResult },
     ...divProps,
   });

+ 37 - 17
reflex/.templates/web/utils/state.js

@@ -179,6 +179,9 @@ export const queueEventIfSocketExists = async (events, socket) => {
 export const applyEvent = async (event, socket, navigate) => {
   // Handle special events
   if (event.name == "_redirect") {
+    if ((event.payload.path ?? undefined) === undefined) {
+      return false;
+    }
     if (event.payload.external) {
       window.open(event.payload.path, "_blank", "noopener");
     } else if (event.payload.replace) {
@@ -241,7 +244,14 @@ export const applyEvent = async (event, socket, navigate) => {
   if (event.name == "_set_focus") {
     const ref =
       event.payload.ref in refs ? refs[event.payload.ref] : event.payload.ref;
-    ref.current.focus();
+    const current = ref?.current;
+    if (current === undefined || current?.focus === undefined) {
+      console.error(
+        `No element found for ref ${event.payload.ref} in _set_focus`,
+      );
+    } else {
+      current.focus();
+    }
     return false;
   }
 
@@ -261,11 +271,15 @@ export const applyEvent = async (event, socket, navigate) => {
     try {
       const eval_result = event.payload.function();
       if (event.payload.callback) {
-        if (!!eval_result && typeof eval_result.then === "function") {
-          event.payload.callback(await eval_result);
-        } else {
-          event.payload.callback(eval_result);
-        }
+        const final_result =
+          !!eval_result && typeof eval_result.then === "function"
+            ? await eval_result
+            : eval_result;
+        const callback =
+          typeof event.payload.callback === "string"
+            ? eval(event.payload.callback)
+            : event.payload.callback;
+        callback(final_result);
       }
     } catch (e) {
       console.log("_call_function", e);
@@ -284,11 +298,15 @@ export const applyEvent = async (event, socket, navigate) => {
           : eval(event.payload.function)();
 
       if (event.payload.callback) {
-        if (!!eval_result && typeof eval_result.then === "function") {
-          eval(event.payload.callback)(await eval_result);
-        } else {
-          eval(event.payload.callback)(eval_result);
-        }
+        const final_result =
+          !!eval_result && typeof eval_result.then === "function"
+            ? await eval_result
+            : eval_result;
+        const callback =
+          typeof event.payload.callback === "string"
+            ? eval(event.payload.callback)
+            : event.payload.callback;
+        callback(final_result);
       }
     } catch (e) {
       console.log("_call_script", e);
@@ -366,7 +384,7 @@ export const queueEvents = async (events, socket, prepend) => {
       ),
     ];
   }
-  event_queue.push(...events);
+  event_queue.push(...events.filter((e) => e !== undefined && e !== null));
   await processEvent(socket.current);
 };
 
@@ -756,11 +774,13 @@ export const useEventLoop = (
 
   // Function to add new events to the event queue.
   const addEvents = (events, args, event_actions) => {
+    const _events = events.filter((e) => e !== undefined && e !== null);
+
     if (!(args instanceof Array)) {
       args = [args];
     }
 
-    event_actions = events.reduce(
+    event_actions = _events.reduce(
       (acc, e) => ({ ...acc, ...e.event_actions }),
       event_actions ?? {},
     );
@@ -773,7 +793,7 @@ export const useEventLoop = (
     if (event_actions?.stopPropagation && _e?.stopPropagation) {
       _e.stopPropagation();
     }
-    const combined_name = events.map((e) => e.name).join("+++");
+    const combined_name = _events.map((e) => e.name).join("+++");
     if (event_actions?.temporal) {
       if (!socket.current || !socket.current.connected) {
         return; // don't queue when the backend is not connected
@@ -789,11 +809,11 @@ export const useEventLoop = (
       // If debounce is used, queue the events after some delay
       debounce(
         combined_name,
-        () => queueEvents(events, socket),
+        () => queueEvents(_events, socket),
         event_actions.debounce,
       );
     } else {
-      queueEvents(events, socket);
+      queueEvents(_events, socket);
     }
   };
 
@@ -956,7 +976,7 @@ export const isTrue = (val) => {
  * @returns True if the value is not null or undefined, false otherwise.
  */
 export const isNotNullOrUndefined = (val) => {
-  return val ?? undefined !== undefined;
+  return (val ?? undefined) !== undefined;
 };
 
 /**

+ 1 - 1
reflex/admin.py

@@ -15,4 +15,4 @@ class AdminDash:
 
     models: list = field(default_factory=list)
     view_overrides: dict = field(default_factory=dict)
-    admin: "Admin | None" = None
+    admin: Admin | None = None

+ 204 - 103
reflex/app.py

@@ -13,45 +13,43 @@ import io
 import json
 import sys
 import traceback
+from collections.abc import AsyncIterator, Callable, Coroutine, Sequence
 from datetime import datetime
 from pathlib import Path
 from timeit import default_timer as timer
 from types import SimpleNamespace
-from typing import (
-    TYPE_CHECKING,
-    Any,
-    AsyncIterator,
-    BinaryIO,
-    Callable,
-    Coroutine,
-    Dict,
-    MutableMapping,
-    Type,
-    get_args,
-    get_type_hints,
-)
+from typing import TYPE_CHECKING, Any, BinaryIO, get_args, get_type_hints
 
-from fastapi import FastAPI, HTTPException, Request
-from fastapi import UploadFile as FastAPIUploadFile
-from fastapi.middleware import cors
-from fastapi.responses import JSONResponse, StreamingResponse
-from fastapi.staticfiles import StaticFiles
+from fastapi import FastAPI
 from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
-from socketio import ASGIApp, AsyncNamespace, AsyncServer
+from socketio import ASGIApp as EngineIOApp
+from socketio import AsyncNamespace, AsyncServer
+from starlette.applications import Starlette
 from starlette.datastructures import Headers
 from starlette.datastructures import UploadFile as StarletteUploadFile
+from starlette.exceptions import HTTPException
+from starlette.middleware import cors
+from starlette.requests import ClientDisconnect, Request
+from starlette.responses import JSONResponse, Response, StreamingResponse
+from starlette.staticfiles import StaticFiles
+from typing_extensions import deprecated
 
 from reflex import constants
 from reflex.admin import AdminDash
 from reflex.app_mixins import AppMixin, LifespanMixin, MiddlewareMixin
 from reflex.compiler import compiler
 from reflex.compiler import utils as compiler_utils
-from reflex.compiler.compiler import ExecutorSafeFunctions, compile_theme
+from reflex.compiler.compiler import (
+    ExecutorSafeFunctions,
+    compile_theme,
+    readable_name_from_component,
+)
 from reflex.components.base.app_wrap import AppWrap
 from reflex.components.base.error_boundary import ErrorBoundary
 from reflex.components.base.fragment import Fragment
 from reflex.components.base.strict_mode import StrictMode
 from reflex.components.component import (
+    CUSTOM_COMPONENTS,
     Component,
     ComponentStyle,
     evaluate_style_namespaces,
@@ -108,6 +106,7 @@ from reflex.utils import (
 )
 from reflex.utils.exec import get_compile_context, is_prod_mode, is_testing_env
 from reflex.utils.imports import ImportVar
+from reflex.utils.types import ASGIApp, Message, Receive, Scope, Send
 
 if TYPE_CHECKING:
     from reflex.vars import Var
@@ -295,6 +294,25 @@ class UnevaluatedPage:
     meta: list[dict[str, str]]
     context: dict[str, Any] | None
 
+    def merged_with(self, other: UnevaluatedPage) -> UnevaluatedPage:
+        """Merge the other page into this one.
+
+        Args:
+            other: The other page to merge with.
+
+        Returns:
+            The merged page.
+        """
+        return dataclasses.replace(
+            self,
+            title=self.title if self.title is not None else other.title,
+            description=self.description
+            if self.description is not None
+            else other.description,
+            on_load=self.on_load if self.on_load is not None else other.on_load,
+            context=self.context if self.context is not None else other.context,
+        )
+
 
 @dataclasses.dataclass()
 class App(MiddlewareMixin, LifespanMixin):
@@ -373,13 +391,13 @@ class App(MiddlewareMixin, LifespanMixin):
     _pages: dict[str, Component] = dataclasses.field(default_factory=dict)
 
     # A mapping of pages which created states as they were being evaluated.
-    _stateful_pages: Dict[str, None] = dataclasses.field(default_factory=dict)
+    _stateful_pages: dict[str, None] = dataclasses.field(default_factory=dict)
 
     # The backend API object.
-    _api: FastAPI | None = None
+    _api: Starlette | None = None
 
     # The state class to use for the app.
-    _state: Type[BaseState] | None = None
+    _state: type[BaseState] | None = None
 
     # Class to manage many client states.
     _state_manager: StateManager | None = None
@@ -411,14 +429,34 @@ class App(MiddlewareMixin, LifespanMixin):
     # Put the toast provider in the app wrap.
     toaster: Component | None = dataclasses.field(default_factory=toast.provider)
 
+    # Transform the ASGI app before running it.
+    api_transformer: (
+        Sequence[Callable[[ASGIApp], ASGIApp] | Starlette]
+        | Callable[[ASGIApp], ASGIApp]
+        | Starlette
+        | None
+    ) = None
+
+    # FastAPI app for compatibility with FastAPI.
+    _cached_fastapi_app: FastAPI | None = None
+
     @property
-    def api(self) -> FastAPI | None:
+    @deprecated("Use `api_transformer=your_fastapi_app` instead.")
+    def api(self) -> FastAPI:
         """Get the backend api.
 
         Returns:
             The backend api.
         """
-        return self._api
+        if self._cached_fastapi_app is None:
+            self._cached_fastapi_app = FastAPI()
+        console.deprecate(
+            feature_name="App.api",
+            reason="Set `api_transformer=your_fastapi_app` instead.",
+            deprecation_version="0.7.9",
+            removal_version="0.8.0",
+        )
+        return self._cached_fastapi_app
 
     @property
     def event_namespace(self) -> EventNamespace | None:
@@ -450,8 +488,8 @@ class App(MiddlewareMixin, LifespanMixin):
             set_breakpoints(self.style.pop("breakpoints"))
 
         # Set up the API.
-        self._api = FastAPI(lifespan=self._run_lifespan_tasks)
-        self._add_cors()
+        self._api = Starlette()
+        App._add_cors(self._api)
         self._add_default_endpoints()
 
         for clz in App.__mro__:
@@ -516,7 +554,7 @@ class App(MiddlewareMixin, LifespanMixin):
             )
 
         # Create the socket app. Note event endpoint constant replaces the default 'socket.io' path.
-        socket_app = ASGIApp(self.sio, socketio_path="")
+        socket_app = EngineIOApp(self.sio, socketio_path="")
         namespace = config.get_event_namespace()
 
         # Create the event namespace and attach the main app. Not related to any paths.
@@ -525,18 +563,16 @@ class App(MiddlewareMixin, LifespanMixin):
         # Register the event namespace with the socket.
         self.sio.register_namespace(self.event_namespace)
         # Mount the socket app with the API.
-        if self.api:
+        if self._api:
 
             class HeaderMiddleware:
                 def __init__(self, app: ASGIApp):
                     self.app = app
 
-                async def __call__(
-                    self, scope: MutableMapping[str, Any], receive: Any, send: Callable
-                ):
+                async def __call__(self, scope: Scope, receive: Receive, send: Send):
                     original_send = send
 
-                    async def modified_send(message: dict):
+                    async def modified_send(message: Message):
                         if message["type"] == "websocket.accept":
                             if scope.get("subprotocols"):
                                 # The following *does* say "subprotocol" instead of "subprotocols", intentionally.
@@ -555,7 +591,7 @@ class App(MiddlewareMixin, LifespanMixin):
                     return await self.app(scope, receive, modified_send)
 
             socket_app_with_headers = HeaderMiddleware(socket_app)
-            self.api.mount(str(constants.Endpoint.EVENT), socket_app_with_headers)
+            self._api.mount(str(constants.Endpoint.EVENT), socket_app_with_headers)
 
         # Check the exception handlers
         self._validate_exception_handlers()
@@ -568,7 +604,7 @@ class App(MiddlewareMixin, LifespanMixin):
         """
         return f"<App state={self._state.__name__ if self._state else None}>"
 
-    def __call__(self) -> FastAPI:
+    def __call__(self) -> ASGIApp:
         """Run the backend api instance.
 
         Raises:
@@ -577,9 +613,6 @@ class App(MiddlewareMixin, LifespanMixin):
         Returns:
             The backend api.
         """
-        if not self.api:
-            raise ValueError("The app has not been initialized.")
-
         # For py3.9 compatibility when redis is used, we MUST add any decorator pages
         # before compiling the app in a thread to avoid event loop error (REF-2172).
         self._apply_decorated_pages()
@@ -591,34 +624,76 @@ class App(MiddlewareMixin, LifespanMixin):
             # Force background compile errors to print eagerly
             lambda f: f.result()
         )
-        # Wait for the compile to finish in prod mode to ensure all optional endpoints are mounted.
-        if is_prod_mode():
-            compile_future.result()
+        # Wait for the compile to finish to ensure all optional endpoints are mounted.
+        compile_future.result()
 
-        return self.api
+        if not self._api:
+            raise ValueError("The app has not been initialized.")
+
+        if self._cached_fastapi_app is not None:
+            asgi_app = self._cached_fastapi_app
+            asgi_app.mount("", self._api)
+            App._add_cors(asgi_app)
+        else:
+            asgi_app = self._api
+
+        if self.api_transformer is not None:
+            api_transformers: Sequence[Starlette | Callable[[ASGIApp], ASGIApp]] = (
+                [self.api_transformer]
+                if not isinstance(self.api_transformer, Sequence)
+                else self.api_transformer
+            )
+
+            for api_transformer in api_transformers:
+                if isinstance(api_transformer, Starlette):
+                    # Mount the api to the fastapi app.
+                    App._add_cors(api_transformer)
+                    api_transformer.mount("", asgi_app)
+                    asgi_app = api_transformer
+                else:
+                    # Transform the asgi app.
+                    asgi_app = api_transformer(asgi_app)
+
+        top_asgi_app = Starlette(lifespan=self._run_lifespan_tasks)
+        top_asgi_app.mount("", asgi_app)
+        App._add_cors(top_asgi_app)
+
+        return top_asgi_app
 
     def _add_default_endpoints(self):
         """Add default api endpoints (ping)."""
         # To test the server.
-        if not self.api:
+        if not self._api:
             return
 
-        self.api.get(str(constants.Endpoint.PING))(ping)
-        self.api.get(str(constants.Endpoint.HEALTH))(health)
+        self._api.add_route(
+            str(constants.Endpoint.PING),
+            ping,
+            methods=["GET"],
+        )
+        self._api.add_route(
+            str(constants.Endpoint.HEALTH),
+            health,
+            methods=["GET"],
+        )
 
     def _add_optional_endpoints(self):
         """Add optional api endpoints (_upload)."""
-        if not self.api:
+        if not self._api:
             return
         upload_is_used_marker = (
             prerequisites.get_backend_dir() / constants.Dirs.UPLOAD_IS_USED
         )
         if Upload.is_used or upload_is_used_marker.exists():
             # To upload files.
-            self.api.post(str(constants.Endpoint.UPLOAD))(upload(self))
+            self._api.add_route(
+                str(constants.Endpoint.UPLOAD),
+                upload(self),
+                methods=["POST"],
+            )
 
             # To access uploaded files.
-            self.api.mount(
+            self._api.mount(
                 str(constants.Endpoint.UPLOAD),
                 StaticFiles(directory=get_upload_dir()),
                 name="uploaded_files",
@@ -627,17 +702,22 @@ class App(MiddlewareMixin, LifespanMixin):
             upload_is_used_marker.parent.mkdir(parents=True, exist_ok=True)
             upload_is_used_marker.touch()
         if codespaces.is_running_in_codespaces():
-            self.api.get(str(constants.Endpoint.AUTH_CODESPACE))(
-                codespaces.auth_codespace
+            self._api.add_route(
+                str(constants.Endpoint.AUTH_CODESPACE),
+                codespaces.auth_codespace,
+                methods=["GET"],
             )
         if environment.REFLEX_ADD_ALL_ROUTES_ENDPOINT.get():
             self.add_all_routes_endpoint()
 
-    def _add_cors(self):
-        """Add CORS middleware to the app."""
-        if not self.api:
-            return
-        self.api.add_middleware(
+    @staticmethod
+    def _add_cors(api: Starlette):
+        """Add CORS middleware to the app.
+
+        Args:
+            api: The Starlette app to add CORS middleware to.
+        """
+        api.add_middleware(
             cors.CORSMiddleware,
             allow_credentials=True,
             allow_methods=["*"],
@@ -730,22 +810,37 @@ class App(MiddlewareMixin, LifespanMixin):
         # Check if the route given is valid
         verify_route_validity(route)
 
-        if route in self._unevaluated_pages and environment.RELOAD_CONFIG.is_set():
-            # when the app is reloaded(typically for app harness tests), we should maintain
-            # the latest render function of a route.This applies typically to decorated pages
-            # since they are only added when app._compile is called.
-            self._unevaluated_pages.pop(route)
+        unevaluated_page = UnevaluatedPage(
+            component=component,
+            route=route,
+            title=title,
+            description=description,
+            image=image,
+            on_load=on_load,
+            meta=meta,
+            context=context,
+        )
 
         if route in self._unevaluated_pages:
-            route_name = (
-                f"`{route}` or `/`"
-                if route == constants.PageNames.INDEX_ROUTE
-                else f"`{route}`"
-            )
-            raise exceptions.RouteValueError(
-                f"Duplicate page route {route_name} already exists. Make sure you do not have two"
-                f" pages with the same route"
-            )
+            if self._unevaluated_pages[route].component is component:
+                unevaluated_page = unevaluated_page.merged_with(
+                    self._unevaluated_pages[route]
+                )
+                console.warn(
+                    f"Page {route} is being redefined with the same component."
+                )
+            else:
+                route_name = (
+                    f"`{route}` or `/`"
+                    if route == constants.PageNames.INDEX_ROUTE
+                    else f"`{route}`"
+                )
+                existing_component = self._unevaluated_pages[route].component
+                raise exceptions.RouteValueError(
+                    f"Tried to add page {readable_name_from_component(component)} with route {route_name} but "
+                    f"page {readable_name_from_component(existing_component)} with the same route already exists. "
+                    "Make sure you do not have two pages with the same route."
+                )
 
         # Setup dynamic args for the route.
         # this state assignment is only required for tests using the deprecated state kwarg for App
@@ -757,16 +852,7 @@ class App(MiddlewareMixin, LifespanMixin):
                 on_load if isinstance(on_load, list) else [on_load]
             )
 
-        self._unevaluated_pages[route] = UnevaluatedPage(
-            component=component,
-            route=route,
-            title=title,
-            description=description,
-            image=image,
-            on_load=on_load,
-            meta=meta,
-            context=context,
-        )
+        self._unevaluated_pages[route] = unevaluated_page
 
     def _compile_page(self, route: str, save_page: bool = True):
         """Compile a page.
@@ -896,7 +982,7 @@ class App(MiddlewareMixin, LifespanMixin):
             return
 
         # Get the admin dash.
-        if not self.api:
+        if not self._api:
             return
 
         admin_dash = self.admin_dash
@@ -917,7 +1003,7 @@ class App(MiddlewareMixin, LifespanMixin):
                 view = admin_dash.view_overrides.get(model, ModelView)
                 admin.add_view(view(model))
 
-            admin.mount_to(self.api)
+            admin.mount_to(self._api)
 
     def _get_frontend_packages(self, imports: dict[str, set[ImportVar]]):
         """Gets the frontend packages to be installed and filters out the unnecessary ones.
@@ -1032,11 +1118,12 @@ class App(MiddlewareMixin, LifespanMixin):
 
         This can move back into `compile_` when py39 support is dropped.
         """
+        app_name = get_config().app_name
         # Add the @rx.page decorated pages to collect on_load events.
-        for render, kwargs in DECORATED_PAGES[get_config().app_name]:
+        for render, kwargs in DECORATED_PAGES[app_name]:
             self.add_page(render, **kwargs)
 
-    def _validate_var_dependencies(self, state: Type[BaseState] | None = None) -> None:
+    def _validate_var_dependencies(self, state: type[BaseState] | None = None) -> None:
         """Validate the dependencies of the vars in the app.
 
         Args:
@@ -1202,9 +1289,8 @@ class App(MiddlewareMixin, LifespanMixin):
 
         progress.advance(task)
 
-        # Track imports and custom components found.
+        # Track imports found.
         all_imports = {}
-        custom_components = set()
 
         # This has to happen before compiling stateful components as that
         # prevents recursive functions from reaching all components.
@@ -1215,9 +1301,6 @@ class App(MiddlewareMixin, LifespanMixin):
             # Add the app wrappers from this component.
             app_wrappers.update(component._get_all_app_wrap_components())
 
-            # Add the custom components from the page to the set.
-            custom_components |= component._get_all_custom_components()
-
         if (toaster := self.toaster) is not None:
             from reflex.components.component import memo
 
@@ -1235,9 +1318,6 @@ class App(MiddlewareMixin, LifespanMixin):
             if component is not None:
                 app_wrappers[key] = component
 
-        for component in app_wrappers.values():
-            custom_components |= component._get_all_custom_components()
-
         if self.error_boundary:
             from reflex.compiler.compiler import into_component
 
@@ -1362,7 +1442,7 @@ class App(MiddlewareMixin, LifespanMixin):
             custom_components_output,
             custom_components_result,
             custom_components_imports,
-        ) = compiler.compile_components(custom_components)
+        ) = compiler.compile_components(set(CUSTOM_COMPONENTS.values()))
         compile_results.append((custom_components_output, custom_components_result))
         all_imports.update(custom_components_imports)
 
@@ -1415,12 +1495,15 @@ class App(MiddlewareMixin, LifespanMixin):
 
     def add_all_routes_endpoint(self):
         """Add an endpoint to the app that returns all the routes."""
-        if not self.api:
+        if not self._api:
             return
 
-        @self.api.get(str(constants.Endpoint.ALL_ROUTES))
-        async def all_routes():
-            return list(self._unevaluated_pages.keys())
+        async def all_routes(_request: Request) -> Response:
+            return JSONResponse(list(self._unevaluated_pages.keys()))
+
+        self._api.add_route(
+            str(constants.Endpoint.ALL_ROUTES), all_routes, methods=["GET"]
+        )
 
     @contextlib.asynccontextmanager
     async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
@@ -1593,7 +1676,7 @@ class App(MiddlewareMixin, LifespanMixin):
 
 
 async def process(
-    app: App, event: Event, sid: str, headers: Dict, client_ip: str
+    app: App, event: Event, sid: str, headers: dict, client_ip: str
 ) -> AsyncIterator[StateUpdate]:
     """Process an event.
 
@@ -1675,18 +1758,24 @@ async def process(
         raise
 
 
-async def ping() -> str:
+async def ping(_request: Request) -> Response:
     """Test API endpoint.
 
+    Args:
+        _request: The Starlette request object.
+
     Returns:
         The response.
     """
-    return "pong"
+    return JSONResponse("pong")
 
 
-async def health() -> JSONResponse:
+async def health(_request: Request) -> JSONResponse:
     """Health check endpoint to assess the status of the database and Redis services.
 
+    Args:
+        _request: The Starlette request object.
+
     Returns:
         JSONResponse: A JSON object with the health status:
             - "status" (bool): Overall health, True if all checks pass.
@@ -1728,12 +1817,11 @@ def upload(app: App):
         The upload function.
     """
 
-    async def upload_file(request: Request, files: list[FastAPIUploadFile]):
+    async def upload_file(request: Request):
         """Upload a file.
 
         Args:
-            request: The FastAPI request object.
-            files: The file(s) to upload.
+            request: The Starlette request object.
 
         Returns:
             StreamingResponse yielding newline-delimited JSON of StateUpdate
@@ -1746,6 +1834,15 @@ def upload(app: App):
         """
         from reflex.utils.exceptions import UploadTypeError, UploadValueError
 
+        # Get the files from the request.
+        try:
+            files = await request.form()
+        except ClientDisconnect:
+            return Response()  # user cancelled
+        files = files.getlist("files")
+        if not files:
+            raise UploadValueError("No files were uploaded.")
+
         token = request.headers.get("reflex-client-token")
         handler = request.headers.get("reflex-event-handler")
 
@@ -1798,6 +1895,10 @@ def upload(app: App):
         # event is handled.
         file_copies = []
         for file in files:
+            if not isinstance(file, StarletteUploadFile):
+                raise UploadValueError(
+                    "Uploaded file is not an UploadFile." + str(file)
+                )
             content_copy = io.BytesIO()
             content_copy.write(await file.read())
             content_copy.seek(0)

+ 3 - 3
reflex/app_mixins/lifespan.py

@@ -7,9 +7,9 @@ import contextlib
 import dataclasses
 import functools
 import inspect
-from typing import Callable, Coroutine
+from collections.abc import Callable, Coroutine
 
-from fastapi import FastAPI
+from starlette.applications import Starlette
 
 from reflex.utils import console
 from reflex.utils.exceptions import InvalidLifespanTaskTypeError
@@ -27,7 +27,7 @@ class LifespanMixin(AppMixin):
     )
 
     @contextlib.asynccontextmanager
-    async def _run_lifespan_tasks(self, app: FastAPI):
+    async def _run_lifespan_tasks(self, app: Starlette):
         running_tasks = []
         try:
             async with contextlib.AsyncExitStack() as stack:

+ 2 - 2
reflex/base.py

@@ -3,14 +3,14 @@
 from __future__ import annotations
 
 import os
-from typing import TYPE_CHECKING, Any, Type
+from typing import TYPE_CHECKING, Any
 
 import pydantic.v1.main as pydantic_main
 from pydantic.v1 import BaseModel
 from pydantic.v1.fields import ModelField
 
 
-def validate_field_name(bases: list[Type["BaseModel"]], field_name: str) -> None:
+def validate_field_name(bases: list[type[BaseModel]], field_name: str) -> None:
     """Ensure that the field's name does not shadow an existing attribute of the model.
 
     Args:

+ 53 - 14
reflex/compiler/compiler.py

@@ -2,9 +2,11 @@
 
 from __future__ import annotations
 
+from collections.abc import Iterable, Sequence
 from datetime import datetime
+from inspect import getmodule
 from pathlib import Path
-from typing import TYPE_CHECKING, Iterable, Sequence, Type
+from typing import TYPE_CHECKING
 
 from reflex import constants
 from reflex.compiler import templates, utils
@@ -28,6 +30,13 @@ from reflex.utils.prerequisites import get_web_dir
 from reflex.vars.base import LiteralVar, Var
 
 
+def _apply_common_imports(
+    imports: dict[str, list[ImportVar]],
+):
+    imports.setdefault("@emotion/react", []).append(ImportVar("jsx"))
+    imports.setdefault("react", []).append(ImportVar("Fragment"))
+
+
 def _compile_document_root(root: Component) -> str:
     """Compile the document root.
 
@@ -38,7 +47,7 @@ def _compile_document_root(root: Component) -> str:
         The compiled document root.
     """
     document_root_imports = root._get_all_imports()
-    document_root_imports.setdefault("@emotion/react", []).append(ImportVar("jsx"))
+    _apply_common_imports(document_root_imports)
     return templates.DOCUMENT_ROOT.render(
         imports=utils.compile_imports(document_root_imports),
         document=root.render(),
@@ -56,7 +65,7 @@ def _normalize_library_name(lib: str) -> str:
     """
     if lib == "react":
         return "React"
-    return lib.replace("@", "").replace("/", "_").replace("-", "_")
+    return lib.replace("$/", "").replace("@", "").replace("/", "_").replace("-", "_")
 
 
 def _compile_app(app_root: Component) -> str:
@@ -72,13 +81,10 @@ def _compile_app(app_root: Component) -> str:
 
     window_libraries = [
         (_normalize_library_name(name), name) for name in bundled_libraries
-    ] + [
-        ("utils_context", f"$/{constants.Dirs.UTILS}/context"),
-        ("utils_state", f"$/{constants.Dirs.UTILS}/state"),
     ]
 
     app_root_imports = app_root._get_all_imports()
-    app_root_imports.setdefault("@emotion/react", []).append(ImportVar("jsx"))
+    _apply_common_imports(app_root_imports)
 
     return templates.APP_ROOT.render(
         imports=utils.compile_imports(app_root_imports),
@@ -102,7 +108,7 @@ def _compile_theme(theme: str) -> str:
     return templates.THEME.render(theme=theme)
 
 
-def _compile_contexts(state: Type[BaseState] | None, theme: Component | None) -> str:
+def _compile_contexts(state: type[BaseState] | None, theme: Component | None) -> str:
     """Compile the initial state and contexts.
 
     Args:
@@ -137,7 +143,7 @@ def _compile_contexts(state: Type[BaseState] | None, theme: Component | None) ->
 
 def _compile_page(
     component: BaseComponent,
-    state: Type[BaseState] | None,
+    state: type[BaseState] | None,
 ) -> str:
     """Compile the component given the app state.
 
@@ -149,7 +155,7 @@ def _compile_page(
         The compiled component.
     """
     imports = component._get_all_imports()
-    imports.setdefault("@emotion/react", []).append(ImportVar("jsx"))
+    _apply_common_imports(imports)
     imports = utils.compile_imports(imports)
 
     # Compile the code to render the component.
@@ -342,6 +348,8 @@ def _compile_components(
         component_renders.append(component_render)
         imports = utils.merge_imports(imports, component_imports)
 
+    _apply_common_imports(imports)
+
     dynamic_imports = {
         comp_import: None
         for comp_render in component_renders
@@ -434,6 +442,8 @@ def _compile_stateful_components(
     all_imports.pop(
         f"$/{constants.Dirs.UTILS}/{constants.PageNames.STATEFUL_COMPONENTS}", None
     )
+    if rendered_components:
+        _apply_common_imports(all_imports)
 
     return templates.STATEFUL_COMPONENTS.render(
         imports=utils.compile_imports(all_imports),
@@ -526,7 +536,7 @@ def compile_theme(style: ComponentStyle) -> tuple[str, str]:
 
 
 def compile_contexts(
-    state: Type[BaseState] | None,
+    state: type[BaseState] | None,
     theme: Component | None,
 ) -> tuple[str, str]:
     """Compile the initial state / context.
@@ -545,7 +555,7 @@ def compile_contexts(
 
 
 def compile_page(
-    path: str, component: BaseComponent, state: Type[BaseState] | None
+    path: str, component: BaseComponent, state: type[BaseState] | None
 ) -> tuple[str, str]:
     """Compile a single page.
 
@@ -685,6 +695,35 @@ def _into_component_once(
     return None
 
 
+def readable_name_from_component(
+    component: Component | ComponentCallable,
+) -> str | None:
+    """Get the readable name of a component.
+
+    Args:
+        component: The component to get the name of.
+
+    Returns:
+        The readable name of the component.
+    """
+    if isinstance(component, Component):
+        return type(component).__name__
+    if isinstance(component, (Var, int, float, str)):
+        return str(component)
+    if isinstance(component, Sequence):
+        return ", ".join(str(c) for c in component)
+    if callable(component):
+        module_name = getattr(component, "__module__", None)
+        if module_name is not None:
+            module = getmodule(component)
+            if module is not None:
+                module_name = module.__name__
+        if module_name is not None:
+            return f"{module_name}.{component.__name__}"
+        return component.__name__
+    return None
+
+
 def into_component(component: Component | ComponentCallable) -> Component:
     """Convert a component to a Component.
 
@@ -749,7 +788,7 @@ def into_component(component: Component | ComponentCallable) -> Component:
 def compile_unevaluated_page(
     route: str,
     page: UnevaluatedPage,
-    state: Type[BaseState] | None = None,
+    state: type[BaseState] | None = None,
     style: ComponentStyle | None = None,
     theme: Component | None = None,
 ) -> tuple[Component, bool]:
@@ -839,7 +878,7 @@ class ExecutorSafeFunctions:
 
     COMPONENTS: dict[str, BaseComponent] = {}
     UNCOMPILED_PAGES: dict[str, UnevaluatedPage] = {}
-    STATE: Type[BaseState] | None = None
+    STATE: type[BaseState] | None = None
 
     @classmethod
     def compile_page(cls, route: str) -> tuple[str, str]:

+ 6 - 5
reflex/compiler/utils.py

@@ -5,9 +5,10 @@ from __future__ import annotations
 import asyncio
 import concurrent.futures
 import traceback
+from collections.abc import Sequence
 from datetime import datetime
 from pathlib import Path
-from typing import Any, Sequence, Type
+from typing import Any
 from urllib.parse import urlparse
 
 from pydantic.v1.fields import ModelField
@@ -173,7 +174,7 @@ def save_error(error: Exception) -> str:
     return str(log_path)
 
 
-def compile_state(state: Type[BaseState]) -> dict:
+def compile_state(state: type[BaseState]) -> dict:
     """Compile the state of the app.
 
     Args:
@@ -206,7 +207,7 @@ def compile_state(state: Type[BaseState]) -> dict:
 def _compile_client_storage_field(
     field: ModelField,
 ) -> tuple[
-    Type[Cookie] | Type[LocalStorage] | Type[SessionStorage] | None,
+    type[Cookie] | type[LocalStorage] | type[SessionStorage] | None,
     dict[str, Any] | None,
 ]:
     """Compile the given cookie, local_storage or session_storage field.
@@ -229,7 +230,7 @@ def _compile_client_storage_field(
 
 
 def _compile_client_storage_recursive(
-    state: Type[BaseState],
+    state: type[BaseState],
 ) -> tuple[dict[str, dict], dict[str, dict], dict[str, dict]]:
     """Compile the client-side storage for the given state recursively.
 
@@ -274,7 +275,7 @@ def _compile_client_storage_recursive(
     return cookies, local_storage, session_storage
 
 
-def compile_client_storage(state: Type[BaseState]) -> dict[str, dict]:
+def compile_client_storage(state: type[BaseState]) -> dict[str, dict]:
     """Compile the client-side storage for the given state.
 
     Args:

+ 10 - 7
reflex/components/base/bare.py

@@ -2,8 +2,8 @@
 
 from __future__ import annotations
 
-import json
-from typing import Any, Iterator, Sequence
+from collections.abc import Iterator, Sequence
+from typing import Any
 
 from reflex.components.component import BaseComponent, Component, ComponentStyle
 from reflex.components.tags import Tag
@@ -169,11 +169,14 @@ class Bare(Component):
         return refs
 
     def _render(self) -> Tag:
-        if isinstance(self.contents, Var):
-            if isinstance(self.contents, (BooleanVar, ObjectVar)):
-                return Tagless(contents=f"{self.contents.to_string()!s}")
-            return Tagless(contents=f"{self.contents!s}")
-        return Tagless(contents=f'"{json.dumps(self.contents)}"')
+        contents = (
+            Var.create(self.contents)
+            if not isinstance(self.contents, Var)
+            else self.contents
+        )
+        if isinstance(contents, (BooleanVar, ObjectVar)):
+            return Tagless(contents=f"{contents.to_string()!s}")
+        return Tagless(contents=f"{contents!s}")
 
     def _add_style_recursive(
         self, style: ComponentStyle, theme: Component | None = None

+ 2 - 4
reflex/components/base/body.py

@@ -1,9 +1,7 @@
 """Display the page body."""
 
-from reflex.components.component import Component
+from reflex.components.el import elements
 
 
-class Body(Component):
+class Body(elements.Body):
     """A body component."""
-
-    tag = "body"

+ 1 - 1
reflex/components/base/error_boundary.py

@@ -33,7 +33,7 @@ def on_error_spec(
 class ErrorBoundary(Component):
     """A React Error Boundary component that catches unhandled frontend exceptions."""
 
-    library = "react-error-boundary"
+    library = "react-error-boundary@6.0.0"
     tag = "ErrorBoundary"
 
     # Fired when the boundary catches an error.

+ 3 - 3
reflex/components/base/link.py

@@ -1,10 +1,10 @@
 """Display the title of the current page."""
 
-from reflex.components.component import Component
+from reflex.components.el.elements.base import BaseHTML
 from reflex.vars.base import Var
 
 
-class RawLink(Component):
+class RawLink(BaseHTML):
     """A component that displays the title of the current page."""
 
     tag = "link"
@@ -16,7 +16,7 @@ class RawLink(Component):
     rel: Var[str]
 
 
-class ScriptTag(Component):
+class ScriptTag(BaseHTML):
     """A script tag with the specified type and source."""
 
     tag = "script"

+ 5 - 9
reflex/components/base/meta.py

@@ -3,14 +3,12 @@
 from __future__ import annotations
 
 from reflex.components.base.bare import Bare
-from reflex.components.component import Component
+from reflex.components.el import elements
 
 
-class Title(Component):
+class Title(elements.Title):
     """A component that displays the title of the current page."""
 
-    tag = "title"
-
     def render(self) -> dict:
         """Render the title component.
 
@@ -26,11 +24,9 @@ class Title(Component):
         return super().render()
 
 
-class Meta(Component):
+class Meta(elements.Meta):
     """A component that displays metadata for the current page."""
 
-    tag = "meta"
-
     # The description of character encoding.
     char_set: str | None = None
 
@@ -47,14 +43,14 @@ class Meta(Component):
     http_equiv: str | None = None
 
 
-class Description(Meta):
+class Description(elements.Meta):
     """A component that displays the title of the current page."""
 
     # The type of the description.
     name: str | None = "description"
 
 
-class Image(Meta):
+class Image(elements.Meta):
     """A component that displays the title of the current page."""
 
     # The type of the image.

+ 96 - 116
reflex/components/component.py

@@ -9,25 +9,11 @@ import functools
 import inspect
 import typing
 from abc import ABC, abstractmethod
-from functools import lru_cache, wraps
+from collections.abc import Callable, Iterator, Mapping, Sequence
+from functools import wraps
 from hashlib import md5
 from types import SimpleNamespace
-from typing import (
-    Any,
-    Callable,
-    ClassVar,
-    Iterator,
-    List,
-    Mapping,
-    Sequence,
-    Set,
-    Type,
-    TypeVar,
-    Union,
-    cast,
-    get_args,
-    get_origin,
-)
+from typing import Any, ClassVar, TypeVar, cast, get_args, get_origin
 
 import pydantic.v1
 from rich.markup import escape
@@ -57,6 +43,7 @@ from reflex.event import (
     no_args_event_spec,
     parse_args_spec,
     run_script,
+    unwrap_var_annotation,
 )
 from reflex.style import Style, format_as_emotion
 from reflex.utils import console, format, imports, types
@@ -64,14 +51,15 @@ from reflex.utils.imports import ImportDict, ImportVar, ParsedImportDict, parse_
 from reflex.vars import VarData
 from reflex.vars.base import (
     CachedVarOperation,
+    LiteralNoneVar,
     LiteralVar,
     Var,
     cached_property_no_lock,
 )
 from reflex.vars.function import ArgsFunctionOperation, FunctionStringVar, FunctionVar
 from reflex.vars.number import ternary_operation
-from reflex.vars.object import ObjectVar
-from reflex.vars.sequence import LiteralArrayVar
+from reflex.vars.object import LiteralObjectVar, ObjectVar
+from reflex.vars.sequence import LiteralArrayVar, LiteralStringVar, StringVar
 
 
 class BaseComponent(Base, ABC):
@@ -180,7 +168,7 @@ def evaluate_style_namespaces(style: ComponentStyle) -> dict:
 
 
 # Map from component to styling.
-ComponentStyle = dict[str | Type[BaseComponent] | Callable | ComponentNamespace, Any]
+ComponentStyle = dict[str | type[BaseComponent] | Callable | ComponentNamespace, Any]
 ComponentChild = types.PrimitiveType | Var | BaseComponent
 ComponentChildTypes = (*types.PrimitiveTypes, Var, BaseComponent, type(None))
 
@@ -227,7 +215,7 @@ def satisfies_type_hint(obj: Any, type_hint: Any) -> bool:
 
 
 def _components_from(
-    component_or_var: Union[BaseComponent, Var],
+    component_or_var: BaseComponent | Var,
 ) -> tuple[BaseComponent, ...]:
     """Get the components from a component or Var.
 
@@ -281,7 +269,7 @@ class Component(BaseComponent, ABC):
     alias: str | None = pydantic.v1.Field(default_factory=lambda: None)
 
     # Whether the component is a global scope tag. True for tags like `html`, `head`, `body`.
-    _is_tag_in_global_scope: bool = pydantic.PrivateAttr(default_factory=lambda: False)
+    _is_tag_in_global_scope: ClassVar[bool] = False
 
     # Whether the import is default or named.
     is_default: bool | None = pydantic.v1.Field(default_factory=lambda: False)
@@ -320,7 +308,7 @@ class Component(BaseComponent, ABC):
     _memoization_mode: MemoizationMode = MemoizationMode()
 
     # State class associated with this component instance
-    State: Type[reflex.state.State] | None = pydantic.v1.Field(
+    State: type[reflex.state.State] | None = pydantic.v1.Field(
         default_factory=lambda: None
     )
 
@@ -614,14 +602,37 @@ class Component(BaseComponent, ABC):
 
         # Convert class_name to str if it's list
         class_name = kwargs.get("class_name", "")
-        if isinstance(class_name, (List, tuple)):
-            if any(isinstance(c, Var) for c in class_name):
+        if isinstance(class_name, (list, tuple)):
+            has_var = False
+            for c in class_name:
+                if isinstance(c, str):
+                    continue
+                if isinstance(c, Var):
+                    if not isinstance(c, StringVar) and not issubclass(
+                        c._var_type, str
+                    ):
+                        raise TypeError(
+                            f"Invalid class_name passed for prop {type(self).__name__}.class_name, expected type str, got value {c._js_expr} of type {c._var_type}."
+                        )
+                    has_var = True
+                else:
+                    raise TypeError(
+                        f"Invalid class_name passed for prop {type(self).__name__}.class_name, expected type str, got value {c} of type {type(c)}."
+                    )
+            if has_var:
                 kwargs["class_name"] = LiteralArrayVar.create(
                     class_name, _var_type=list[str]
                 ).join(" ")
             else:
                 kwargs["class_name"] = " ".join(class_name)
-
+        elif (
+            isinstance(class_name, Var)
+            and not isinstance(class_name, StringVar)
+            and not issubclass(class_name._var_type, str)
+        ):
+            raise TypeError(
+                f"Invalid class_name passed for prop {type(self).__name__}.class_name, expected type str, got value {class_name._js_expr} of type {class_name._var_type}."
+            )
         # Construct the component.
         for key, value in kwargs.items():
             setattr(self, key, value)
@@ -681,6 +692,7 @@ class Component(BaseComponent, ABC):
         Returns:
             The tag to render.
         """
+        # Create the base tag.
         name = (self.tag if not self.alias else self.alias) or ""
         if self._is_tag_in_global_scope and self.library is None:
             name = '"' + name + '"'
@@ -724,7 +736,7 @@ class Component(BaseComponent, ABC):
         return tag.add_props(**props)
 
     @classmethod
-    @lru_cache(maxsize=None)
+    @functools.cache
     def get_props(cls) -> set[str]:
         """Get the unique fields for the component.
 
@@ -734,7 +746,7 @@ class Component(BaseComponent, ABC):
         return set(cls.get_fields()) - set(Component.get_fields())
 
     @classmethod
-    @lru_cache(maxsize=None)
+    @functools.cache
     def get_initial_props(cls) -> set[str]:
         """Get the initial props to set for the component.
 
@@ -753,8 +765,8 @@ class Component(BaseComponent, ABC):
         return True
 
     @classmethod
-    @lru_cache(maxsize=None)
-    def _get_component_prop_names(cls) -> Set[str]:
+    @functools.cache
+    def _get_component_prop_names(cls) -> set[str]:
         """Get the names of the component props. NOTE: This assumes all fields are known.
 
         Returns:
@@ -790,7 +802,7 @@ class Component(BaseComponent, ABC):
         ]
 
     @classmethod
-    def create(cls: Type[T], *children, **props) -> T:
+    def create(cls: type[T], *children, **props) -> T:
         """Create the component.
 
         Args:
@@ -838,7 +850,7 @@ class Component(BaseComponent, ABC):
         return cls._create(children_normalized, **props)
 
     @classmethod
-    def _create(cls: Type[T], children: Sequence[BaseComponent], **props: Any) -> T:
+    def _create(cls: type[T], children: Sequence[BaseComponent], **props: Any) -> T:
         """Create the component.
 
         Args:
@@ -854,7 +866,7 @@ class Component(BaseComponent, ABC):
 
     @classmethod
     def _unsafe_create(
-        cls: Type[T], children: Sequence[BaseComponent], **props: Any
+        cls: type[T], children: Sequence[BaseComponent], **props: Any
     ) -> T:
         """Create the component without running post_init.
 
@@ -1167,7 +1179,7 @@ class Component(BaseComponent, ABC):
                 vars.append(comp_prop)
             elif isinstance(comp_prop, str):
                 # Collapse VarData encoded in f-strings.
-                var = LiteralVar.create(comp_prop)
+                var = LiteralStringVar.create(comp_prop)
                 if var._get_all_var_data() is not None:
                     vars.append(var)
 
@@ -1233,7 +1245,7 @@ class Component(BaseComponent, ABC):
             yield clz.__name__
 
     @classmethod
-    def _iter_parent_classes_with_method(cls, method: str) -> Iterator[Type[Component]]:
+    def _iter_parent_classes_with_method(cls, method: str) -> Iterator[type[Component]]:
         """Iterate through parent classes that define a given method.
 
         Used for handling the `add_*` API functions that internally simulate a super() call chain.
@@ -1668,32 +1680,6 @@ class Component(BaseComponent, ABC):
 
         return refs
 
-    def _get_all_custom_components(
-        self, seen: set[str] | None = None
-    ) -> set[CustomComponent]:
-        """Get all the custom components used by the component.
-
-        Args:
-            seen: The tags of the components that have already been seen.
-
-        Returns:
-            The set of custom components.
-        """
-        custom_components = set()
-
-        # Store the seen components in a set to avoid infinite recursion.
-        if seen is None:
-            seen = set()
-        for child in self.children:
-            # Skip BaseComponent and StatefulComponent children.
-            if not isinstance(child, Component):
-                continue
-            custom_components |= child._get_all_custom_components(seen=seen)
-        for component in self._get_components_in_props():
-            if isinstance(component, Component) and component.tag is not None:
-                custom_components |= component._get_all_custom_components(seen=seen)
-        return custom_components
-
     @property
     def import_var(self):
         """The tag to import.
@@ -1786,11 +1772,9 @@ class CustomComponent(Component):
                 else (
                     annotation_args[1]
                     if get_origin(
-                        (
-                            annotation := inspect.getfullargspec(
-                                component_fn
-                            ).annotations[key]
-                        )
+                        annotation := inspect.getfullargspec(component_fn).annotations[
+                            key
+                        ]
                     )
                     is typing.Annotated
                     and (annotation_args := get_args(annotation))
@@ -1880,37 +1864,6 @@ class CustomComponent(Component):
         """
         return set()
 
-    def _get_all_custom_components(
-        self, seen: set[str] | None = None
-    ) -> set[CustomComponent]:
-        """Get all the custom components used by the component.
-
-        Args:
-            seen: The tags of the components that have already been seen.
-
-        Raises:
-            ValueError: If the tag is not set.
-
-        Returns:
-            The set of custom components.
-        """
-        if self.tag is None:
-            raise ValueError("The tag must be set.")
-
-        # Store the seen components in a set to avoid infinite recursion.
-        if seen is None:
-            seen = set()
-        custom_components = {self} | super()._get_all_custom_components(seen=seen)
-
-        # Avoid adding the same component twice.
-        if self.tag not in seen:
-            seen.add(self.tag)
-            custom_components |= self.get_component(self)._get_all_custom_components(
-                seen=seen
-            )
-
-        return custom_components
-
     @staticmethod
     def _get_event_spec_from_args_spec(name: str, event: EventChain) -> Callable:
         """Get the event spec from the args spec.
@@ -1948,7 +1901,7 @@ class CustomComponent(Component):
 
         return fn
 
-    def get_prop_vars(self) -> List[Var | Callable]:
+    def get_prop_vars(self) -> list[Var | Callable]:
         """Get the prop vars.
 
         Returns:
@@ -1964,7 +1917,7 @@ class CustomComponent(Component):
             for name, prop in self.props.items()
         ]
 
-    @lru_cache(maxsize=None)  # noqa: B019
+    @functools.cache  # noqa: B019
     def get_component(self) -> Component:
         """Render the component.
 
@@ -1974,6 +1927,42 @@ class CustomComponent(Component):
         return self.component_fn(*self.get_prop_vars())
 
 
+CUSTOM_COMPONENTS: dict[str, CustomComponent] = {}
+
+
+def _register_custom_component(
+    component_fn: Callable[..., Component],
+):
+    """Register a custom component to be compiled.
+
+    Args:
+        component_fn: The function that creates the component.
+
+    Raises:
+        TypeError: If the tag name cannot be determined.
+    """
+    dummy_props = {
+        prop: (
+            Var(
+                "",
+                _var_type=unwrap_var_annotation(annotation),
+            ).guess_type()
+            if not types.safe_issubclass(annotation, EventHandler)
+            else EventSpec(handler=EventHandler(fn=lambda: []))
+        )
+        for prop, annotation in typing.get_type_hints(component_fn).items()
+        if prop != "return"
+    }
+    dummy_component = CustomComponent._create(
+        children=[],
+        component_fn=component_fn,
+        **dummy_props,
+    )
+    if dummy_component.tag is None:
+        raise TypeError(f"Could not determine the tag name for {component_fn!r}")
+    CUSTOM_COMPONENTS[dummy_component.tag] = dummy_component
+
+
 def custom_component(
     component_fn: Callable[..., Component],
 ) -> Callable[..., CustomComponent]:
@@ -1994,6 +1983,9 @@ def custom_component(
             children=list(children), component_fn=component_fn, **props
         )
 
+    # Register this component so it can be compiled.
+    _register_custom_component(component_fn)
+
     return wrapper
 
 
@@ -2540,7 +2532,7 @@ def render_dict_to_var(tag: dict | Component | str, imported_names: set[str]) ->
         return Var.create(tag)
 
     if "iterable" in tag:
-        function_return = Var.create(
+        function_return = LiteralArrayVar.create(
             [
                 render_dict_to_var(child.render(), imported_names)
                 for child in tag["children"]
@@ -2583,7 +2575,7 @@ def render_dict_to_var(tag: dict | Component | str, imported_names: set[str]) ->
             render_dict_to_var(tag["true_value"], imported_names),
             render_dict_to_var(tag["false_value"], imported_names)
             if tag["false_value"] is not None
-            else Var.create(None),
+            else LiteralNoneVar.create(),
         )
 
     props = {}
@@ -2599,7 +2591,9 @@ def render_dict_to_var(tag: dict | Component | str, imported_names: set[str]) ->
         value = prop_str[prop + 1 :]
         props[key] = value
 
-    props = Var.create({Var.create(k): Var(v) for k, v in props.items()})
+    props = LiteralObjectVar.create(
+        {LiteralStringVar.create(k): Var(v) for k, v in props.items()}
+    )
 
     for prop in special_props:
         props = props.merge(prop)
@@ -2653,23 +2647,9 @@ class LiteralComponentVar(CachedVarOperation, LiteralVar, ComponentVar):
         """
         return VarData.merge(
             self._var_data,
-            VarData(
-                imports={
-                    "@emotion/react": [
-                        ImportVar(tag="jsx"),
-                    ],
-                }
-            ),
             VarData(
                 imports=self._var_value._get_all_imports(),
             ),
-            VarData(
-                imports={
-                    "react": [
-                        ImportVar(tag="Fragment"),
-                    ],
-                }
-            ),
         )
 
     def __hash__(self) -> int:

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

@@ -74,7 +74,7 @@ class Breakpoints(dict[K, V]):
         thresholds = [initial, xs, sm, md, lg, xl]
 
         if custom is not None:
-            if any((threshold is not None for threshold in thresholds)):
+            if any(threshold is not None for threshold in thresholds):
                 raise ValueError("Named props cannot be used with custom thresholds")
 
             return Breakpoints(custom)

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

@@ -2,7 +2,7 @@
 
 from __future__ import annotations
 
-from typing import Sequence
+from collections.abc import Sequence
 
 from reflex.components.base.fragment import Fragment
 from reflex.components.tags.tag import Tag

+ 35 - 4
reflex/components/core/colors.py

@@ -1,11 +1,22 @@
 """The colors used in Reflex are a wrapper around https://www.radix-ui.com/colors."""
 
-from reflex.constants.colors import Color, ColorType, ShadeType
-from reflex.utils.types import validate_parameter_literals
+from reflex.constants.base import REFLEX_VAR_OPENING_TAG
+from reflex.constants.colors import (
+    COLORS,
+    MAX_SHADE_VALUE,
+    MIN_SHADE_VALUE,
+    Color,
+    ColorType,
+    ShadeType,
+)
+from reflex.vars.base import Var
 
 
-@validate_parameter_literals
-def color(color: ColorType, shade: ShadeType = 7, alpha: bool = False) -> Color:
+def color(
+    color: ColorType | Var[str],
+    shade: ShadeType | Var[int] = 7,
+    alpha: bool | Var[bool] = False,
+) -> Color:
     """Create a color object.
 
     Args:
@@ -15,5 +26,25 @@ def color(color: ColorType, shade: ShadeType = 7, alpha: bool = False) -> Color:
 
     Returns:
         The color object.
+
+    Raises:
+        ValueError: If the color, shade, or alpha are not valid.
     """
+    if isinstance(color, str):
+        if color not in COLORS and REFLEX_VAR_OPENING_TAG not in color:
+            raise ValueError(f"Color must be one of {COLORS}, received {color}")
+    elif not isinstance(color, Var):
+        raise ValueError("Color must be a string or a Var")
+
+    if isinstance(shade, int):
+        if shade < MIN_SHADE_VALUE or shade > MAX_SHADE_VALUE:
+            raise ValueError(
+                f"Shade must be between {MIN_SHADE_VALUE} and {MAX_SHADE_VALUE}"
+            )
+    elif not isinstance(shade, Var):
+        raise ValueError("Shade must be an integer or a Var")
+
+    if not isinstance(alpha, (bool, Var)):
+        raise ValueError("Alpha must be a boolean or a Var")
+
     return Color(color, shade, alpha)

+ 2 - 2
reflex/components/core/cond.py

@@ -2,7 +2,7 @@
 
 from __future__ import annotations
 
-from typing import Any, Dict, overload
+from typing import Any, overload
 
 from reflex.components.base.fragment import Fragment
 from reflex.components.component import BaseComponent, Component, MemoizationLeaf
@@ -66,7 +66,7 @@ class Cond(MemoizationLeaf):
             false_value=self.children[1].render(),
         )
 
-    def render(self) -> Dict:
+    def render(self) -> dict:
         """Render the component.
 
         Returns:

+ 3 - 3
reflex/components/core/debounce.py

@@ -2,7 +2,7 @@
 
 from __future__ import annotations
 
-from typing import Any, Type
+from typing import Any
 
 from reflex.components.component import Component
 from reflex.constants import EventTriggers
@@ -43,7 +43,7 @@ class DebounceInput(Component):
     input_ref: Var[str]
 
     # The element to wrap
-    element: Var[Type[Component]]
+    element: Var[type[Component]]
 
     # Fired when the input value changes
     on_change: EventHandler[no_args_event_spec]
@@ -115,7 +115,7 @@ class DebounceInput(Component):
             "element",
             Var(
                 _js_expr=str(child.alias or child.tag),
-                _var_type=Type[Component],
+                _var_type=type[Component],
                 _var_data=VarData(
                     imports=child._get_imports(),
                     hooks=child._get_all_hooks(),

+ 2 - 1
reflex/components/core/foreach.py

@@ -4,7 +4,8 @@ from __future__ import annotations
 
 import functools
 import inspect
-from typing import Any, Callable, Iterable
+from collections.abc import Callable, Iterable
+from typing import Any
 
 from reflex.components.base.fragment import Fragment
 from reflex.components.component import Component

+ 2 - 2
reflex/components/core/match.py

@@ -1,7 +1,7 @@
 """rx.match."""
 
 import textwrap
-from typing import Any, Dict
+from typing import Any
 
 from reflex.components.base import Fragment
 from reflex.components.component import BaseComponent, Component, MemoizationLeaf
@@ -247,7 +247,7 @@ class Match(MemoizationLeaf):
             cond=self.cond, match_cases=self.match_cases, default=self.default
         )
 
-    def render(self) -> Dict:
+    def render(self) -> dict:
         """Render the component.
 
         Returns:

+ 51 - 14
reflex/components/core/upload.py

@@ -2,8 +2,9 @@
 
 from __future__ import annotations
 
+from collections.abc import Callable, Sequence
 from pathlib import Path
-from typing import Any, Callable, ClassVar, Sequence
+from typing import Any, ClassVar
 
 from reflex.components.base.fragment import Fragment
 from reflex.components.component import (
@@ -12,6 +13,7 @@ from reflex.components.component import (
     MemoizationLeaf,
     StatefulComponent,
 )
+from reflex.components.core.cond import cond
 from reflex.components.el.elements.forms import Input
 from reflex.components.radix.themes.layout.box import Box
 from reflex.config import environment
@@ -23,9 +25,11 @@ from reflex.event import (
     EventHandler,
     EventSpec,
     call_event_fn,
+    call_event_handler,
     parse_args_spec,
     run_script,
 )
+from reflex.style import Style
 from reflex.utils import format
 from reflex.utils.imports import ImportVar
 from reflex.vars import VarData
@@ -229,6 +233,9 @@ class Upload(MemoizationLeaf):
     # Fired when files are dropped.
     on_drop: EventHandler[_on_drop_spec]
 
+    # Style rules to apply when actively dragging.
+    drag_active_style: Style | None = None
+
     @classmethod
     def create(cls, *children, **props) -> Component:
         """Create an upload component.
@@ -264,22 +271,46 @@ class Upload(MemoizationLeaf):
             # If on_drop is not provided, save files to be uploaded later.
             upload_props["on_drop"] = upload_file(upload_props["id"])
         else:
-            on_drop = upload_props["on_drop"]
-            if isinstance(on_drop, Callable):
-                # Call the lambda to get the event chain.
-                on_drop = call_event_fn(on_drop, _on_drop_spec)
-            if isinstance(on_drop, EventSpec):
-                # Update the provided args for direct use with on_drop.
-                on_drop = on_drop.with_args(
-                    args=tuple(
-                        cls._update_arg_tuple_for_on_drop(arg_value)
-                        for arg_value in on_drop.args
-                    ),
-                )
+            on_drop = (
+                [on_drop_prop]
+                if not isinstance(on_drop_prop := upload_props["on_drop"], Sequence)
+                else list(on_drop_prop)
+            )
+            for ix, event in enumerate(on_drop):
+                if isinstance(event, (EventHandler, EventSpec)):
+                    # Call the lambda to get the event chain.
+                    event = call_event_handler(event, _on_drop_spec)
+                elif isinstance(event, Callable):
+                    # Call the lambda to get the event chain.
+                    event = call_event_fn(event, _on_drop_spec)
+                if isinstance(event, EventSpec):
+                    # Update the provided args for direct use with on_drop.
+                    event = event.with_args(
+                        args=tuple(
+                            cls._update_arg_tuple_for_on_drop(arg_value)
+                            for arg_value in event.args
+                        ),
+                    )
+                on_drop[ix] = event
             upload_props["on_drop"] = on_drop
 
         input_props_unique_name = get_unique_variable_name()
         root_props_unique_name = get_unique_variable_name()
+        is_drag_active_unique_name = get_unique_variable_name()
+        drag_active_css_class_unique_name = get_unique_variable_name() + "-drag-active"
+
+        # Handle special style when dragging over the drop zone.
+        if "drag_active_style" in props:
+            props.setdefault("style", Style())[
+                f"&:where(.{drag_active_css_class_unique_name})"
+            ] = props.pop("drag_active_style")
+            props["class_name"].append(
+                cond(
+                    Var(is_drag_active_unique_name),
+                    drag_active_css_class_unique_name,
+                    "",
+                ),
+            )
 
         event_var, callback_str = StatefulComponent._get_memoized_event_triggers(
             GhostUpload.create(on_drop=upload_props["on_drop"])
@@ -298,7 +329,13 @@ class Upload(MemoizationLeaf):
             }
         )
 
-        left_side = f"const {{getRootProps: {root_props_unique_name}, getInputProps: {input_props_unique_name}}} "
+        left_side = (
+            "const { "
+            f"getRootProps: {root_props_unique_name}, "
+            f"getInputProps: {input_props_unique_name}, "
+            f"isDragActive: {is_drag_active_unique_name}"
+            "}"
+        )
         right_side = f"useDropzone({use_dropzone_arguments!s})"
 
         var_data = VarData.merge(

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

@@ -2,8 +2,9 @@
 
 from __future__ import annotations
 
+from collections.abc import Mapping, Sequence
 from enum import Enum
-from typing import Any, Dict, Literal, Mapping, Sequence, TypedDict
+from typing import Any, Literal, TypedDict
 
 from reflex.base import Base
 from reflex.components.component import Component, NoSSRComponent
@@ -257,7 +258,7 @@ class DataEditor(NoSSRComponent):
     scroll_offset_y: Var[int]
 
     # global theme
-    theme: Var[DataEditorTheme | Dict]
+    theme: Var[DataEditorTheme | dict]
 
     # Fired when a cell is activated.
     on_cell_activated: EventHandler[passthrough_event_spec(tuple[int, int])]

+ 2 - 2
reflex/components/datadisplay/shiki_code_block.py

@@ -421,7 +421,7 @@ class ShikiBaseTransformers(Base):
 class ShikiJsTransformer(ShikiBaseTransformers):
     """A Wrapped shikijs transformer."""
 
-    library: str = "@shikijs/transformers"
+    library: str = "@shikijs/transformers@3.3.0"
     fns: list[FunctionStringVar] = [
         FunctionStringVar.create(fn) for fn in SHIKIJS_TRANSFORMER_FNS
     ]
@@ -538,7 +538,7 @@ class ShikiCodeBlock(Component, MarkdownComponentMap):
 
     alias = "ShikiCode"
 
-    lib_dependencies: list[str] = ["shiki"]
+    lib_dependencies: list[str] = ["shiki@3.3.0"]
 
     # The language to use.
     language: Var[LiteralCodeLanguage] = Var.create("python")

+ 13 - 3
reflex/components/dynamic.py

@@ -26,7 +26,14 @@ def get_cdn_url(lib: str) -> str:
     return f"https://cdn.jsdelivr.net/npm/{lib}" + "/+esm"
 
 
-bundled_libraries = {"react", "@radix-ui/themes", "@emotion/react"}
+bundled_libraries = {
+    "react",
+    "@radix-ui/themes",
+    "@emotion/react",
+    f"$/{constants.Dirs.UTILS}/context",
+    f"$/{constants.Dirs.UTILS}/state",
+    f"$/{constants.Dirs.UTILS}/components",
+}
 
 
 def bundle_library(component: Union["Component", str]):
@@ -64,7 +71,7 @@ def load_dynamic_serializer():
             The generated code
         """
         # Causes a circular import, so we import here.
-        from reflex.compiler import templates, utils
+        from reflex.compiler import compiler, templates, utils
         from reflex.components.base.bare import Bare
 
         component = Bare.create(Var.create(component))
@@ -89,8 +96,11 @@ def load_dynamic_serializer():
 
         libs_in_window = bundled_libraries
 
+        component_imports = component._get_all_imports()
+        compiler._apply_common_imports(component_imports)
+
         imports = {}
-        for lib, names in component._get_all_imports().items():
+        for lib, names in component_imports.items():
             formatted_lib_name = format_library_name(lib)
             if (
                 not lib.startswith((".", "/", "$/"))

+ 2 - 2
reflex/components/el/element.py

@@ -1,6 +1,6 @@
 """Base class definition for raw HTML elements."""
 
-import pydantic
+from typing import ClassVar
 
 from reflex.components.component import Component
 
@@ -8,7 +8,7 @@ from reflex.components.component import Component
 class Element(Component):
     """The base class for all raw HTML elements."""
 
-    _is_tag_in_global_scope = pydantic.PrivateAttr(default_factory=lambda: True)
+    _is_tag_in_global_scope: ClassVar[bool] = True
 
     def __eq__(self, other: object):
         """Two elements are equal if they have the same tag.

+ 12 - 8
reflex/components/el/elements/forms.py

@@ -2,8 +2,9 @@
 
 from __future__ import annotations
 
+from collections.abc import Iterator
 from hashlib import md5
-from typing import Any, Iterator, Literal
+from typing import Any, ClassVar, Literal
 
 from jinja2 import Environment
 
@@ -85,6 +86,8 @@ class Button(BaseHTML):
     # Value of the button, used when sending form data
     value: Var[str | int | float]
 
+    _invalid_children: ClassVar[list[str]] = ["Button"]
+
 
 class Datalist(BaseHTML):
     """Display the datalist element."""
@@ -460,15 +463,16 @@ class Input(BaseInput):
                 value_var.is_not_none(), value_var, Var.create("")
             )
 
-        input_type = props.get("type")
+        if cls is Input:
+            input_type = props.get("type")
 
-        if input_type == "checkbox":
-            # Checkbox inputs should use the CheckboxInput class
-            return CheckboxInput.create(*children, **props)
+            if input_type == "checkbox":
+                # Checkbox inputs should use the CheckboxInput class
+                return CheckboxInput.create(*children, **props)
 
-        if input_type == "number" or input_type == "range":
-            # Number inputs should use the ValueNumberInput class
-            return ValueNumberInput.create(*children, **props)
+            if input_type == "number" or input_type == "range":
+                # Number inputs should use the ValueNumberInput class
+                return ValueNumberInput.create(*children, **props)
 
         return super().create(*children, **props)
 

+ 3 - 1
reflex/components/el/elements/inline.py

@@ -1,6 +1,6 @@
 """Inline classes."""
 
-from typing import Literal
+from typing import ClassVar, Literal
 
 from reflex.vars.base import Var
 
@@ -48,6 +48,8 @@ class A(BaseHTML):  # Inherits common attributes from BaseMeta
     # Specifies where to open the linked document
     target: Var[str | Literal["_self", "_blank", "_parent", "_top"]]
 
+    _invalid_children: ClassVar[list[str]] = ["A"]
+
 
 class Abbr(BaseHTML):
     """Display the abbr element."""

+ 3 - 1
reflex/components/el/elements/typography.py

@@ -1,6 +1,6 @@
 """Typography classes."""
 
-from typing import Literal
+from typing import ClassVar, Literal
 
 from reflex.vars.base import Var
 
@@ -87,6 +87,8 @@ class P(BaseHTML):
 
     tag = "p"
 
+    _invalid_children: ClassVar[list] = ["P", "Ol", "Ul", "Div"]
+
 
 class Pre(BaseHTML):
     """Display the pre element."""

+ 3 - 2
reflex/components/gridjs/datatable.py

@@ -2,7 +2,8 @@
 
 from __future__ import annotations
 
-from typing import Any, Dict, Sequence
+from collections.abc import Sequence
+from typing import Any
 
 from reflex.components.component import Component
 from reflex.components.tags import Tag
@@ -44,7 +45,7 @@ class DataTable(Gridjs):
     resizable: Var[bool]
 
     # Enable pagination.
-    pagination: Var[bool | Dict]
+    pagination: Var[bool | dict]
 
     @classmethod
     def create(cls, *children, **props):

+ 46 - 3
reflex/components/lucide/icon.py

@@ -10,7 +10,7 @@ from reflex.vars.sequence import LiteralStringVar, StringVar
 class LucideIconComponent(Component):
     """Lucide Icon Component."""
 
-    library = "lucide-react@0.471.1"
+    library = "lucide-react@0.507.0"
 
 
 class Icon(LucideIconComponent):
@@ -75,13 +75,12 @@ class Icon(LucideIconComponent):
             )
             console.warn(
                 f"Invalid icon tag: {tag}. Please use one of the following: {', '.join(icons_sorted[0:10])}, ..."
-                "\nSee full list at https://reflex.dev/docs/library/data-display/icon/#icons-list. Using 'circle-help' icon instead."
+                "\nSee full list at https://reflex.dev/docs/library/data-display/icon/#icons-list. Using 'circle_help' icon instead."
             )
             tag = "circle_help"
 
         props["tag"] = LUCIDE_ICON_MAPPING_OVERRIDE.get(tag, format.to_title_case(tag))
         props["alias"] = f"Lucide{props['tag']}"
-        props.setdefault("color", "var(--current-color)")
         return super().create(**props)
 
 
@@ -234,6 +233,8 @@ LUCIDE_ICON_LIST = [
     "banana",
     "bandage",
     "banknote",
+    "banknote_arrow_down",
+    "banknote_x",
     "bar_chart",
     "bar_chart_2",
     "bar_chart_3",
@@ -249,6 +250,7 @@ LUCIDE_ICON_LIST = [
     "battery_full",
     "battery_low",
     "battery_medium",
+    "battery_plus",
     "battery_warning",
     "beaker",
     "bean",
@@ -321,6 +323,7 @@ LUCIDE_ICON_LIST = [
     "bot",
     "bot_message_square",
     "bot_off",
+    "bow_arrow",
     "box",
     "box_select",
     "boxes",
@@ -330,12 +333,15 @@ LUCIDE_ICON_LIST = [
     "brain_circuit",
     "brain_cog",
     "brick_wall",
+    "brick_wall_fire",
     "briefcase",
     "briefcase_business",
     "briefcase_conveyor_belt",
     "briefcase_medical",
     "bring_to_front",
     "brush",
+    "brush_cleaning",
+    "bubbles",
     "bug",
     "bug_off",
     "bug_play",
@@ -475,6 +481,7 @@ LUCIDE_ICON_LIST = [
     "circle_power",
     "circle_slash",
     "circle_slash_2",
+    "circle_small",
     "circle_stop",
     "circle_user",
     "circle_user_round",
@@ -509,6 +516,8 @@ LUCIDE_ICON_LIST = [
     "clock_alert",
     "clock_arrow_down",
     "clock_arrow_up",
+    "clock_fading",
+    "clock_plus",
     "cloud",
     "cloud_alert",
     "cloud_cog",
@@ -538,6 +547,7 @@ LUCIDE_ICON_LIST = [
     "coins",
     "columns_2",
     "columns_3",
+    "columns_3_cog",
     "columns_4",
     "combine",
     "command",
@@ -585,6 +595,8 @@ LUCIDE_ICON_LIST = [
     "database",
     "database_backup",
     "database_zap",
+    "decimals_arrow_left",
+    "decimals_arrow_right",
     "delete",
     "dessert",
     "diameter",
@@ -612,6 +624,7 @@ LUCIDE_ICON_LIST = [
     "dollar_sign",
     "donut",
     "door_closed",
+    "door_closed_locked",
     "door_open",
     "dot",
     "download",
@@ -785,6 +798,9 @@ LUCIDE_ICON_LIST = [
     "frown",
     "fuel",
     "fullscreen",
+    "funnel",
+    "funnel_plus",
+    "funnel_x",
     "gallery_horizontal",
     "gallery_horizontal_end",
     "gallery_thumbnails",
@@ -836,6 +852,7 @@ LUCIDE_ICON_LIST = [
     "group",
     "guitar",
     "ham",
+    "hamburger",
     "hammer",
     "hand",
     "hand_coins",
@@ -864,7 +881,9 @@ LUCIDE_ICON_LIST = [
     "heart",
     "heart_crack",
     "heart_handshake",
+    "heart_minus",
     "heart_off",
+    "heart_plus",
     "heart_pulse",
     "heater",
     "hexagon",
@@ -975,6 +994,7 @@ LUCIDE_ICON_LIST = [
     "locate",
     "locate_fixed",
     "locate_off",
+    "location_edit",
     "lock",
     "lock_keyhole",
     "lock_keyhole_open",
@@ -1009,6 +1029,8 @@ LUCIDE_ICON_LIST = [
     "map_pin_x",
     "map_pin_x_inside",
     "map_pinned",
+    "map_plus",
+    "mars_stroke",
     "martini",
     "maximize",
     "maximize_2",
@@ -1107,6 +1129,7 @@ LUCIDE_ICON_LIST = [
     "network",
     "newspaper",
     "nfc",
+    "non_binary",
     "notebook",
     "notebook_pen",
     "notebook_tabs",
@@ -1249,6 +1272,7 @@ LUCIDE_ICON_LIST = [
     "receipt_swiss_franc",
     "receipt_text",
     "rectangle_ellipsis",
+    "rectangle_goggles",
     "rectangle_horizontal",
     "rectangle_vertical",
     "recycle",
@@ -1276,6 +1300,7 @@ LUCIDE_ICON_LIST = [
     "roller_coaster",
     "rotate_3d",
     "rotate_ccw",
+    "rotate_ccw_key",
     "rotate_ccw_square",
     "rotate_cw",
     "rotate_cw_square",
@@ -1287,12 +1312,14 @@ LUCIDE_ICON_LIST = [
     "rows_4",
     "rss",
     "ruler",
+    "ruler_dimension_line",
     "russian_ruble",
     "sailboat",
     "salad",
     "sandwich",
     "satellite",
     "satellite_dish",
+    "saudi_riyal",
     "save",
     "save_all",
     "save_off",
@@ -1348,6 +1375,7 @@ LUCIDE_ICON_LIST = [
     "shield_off",
     "shield_plus",
     "shield_question",
+    "shield_user",
     "shield_x",
     "ship",
     "ship_wheel",
@@ -1357,6 +1385,8 @@ LUCIDE_ICON_LIST = [
     "shopping_cart",
     "shovel",
     "shower_head",
+    "shredder",
+    "shrimp",
     "shrink",
     "shrub",
     "shuffle",
@@ -1385,6 +1415,7 @@ LUCIDE_ICON_LIST = [
     "smile_plus",
     "snail",
     "snowflake",
+    "soap_dispenser_droplet",
     "sofa",
     "soup",
     "space",
@@ -1396,6 +1427,7 @@ LUCIDE_ICON_LIST = [
     "spell_check",
     "spell_check_2",
     "spline",
+    "spline_pointer",
     "split",
     "spray_can",
     "sprout",
@@ -1449,6 +1481,7 @@ LUCIDE_ICON_LIST = [
     "square_plus",
     "square_power",
     "square_radical",
+    "square_round_corner",
     "square_scissors",
     "square_sigma",
     "square_slash",
@@ -1460,6 +1493,10 @@ LUCIDE_ICON_LIST = [
     "square_user",
     "square_user_round",
     "square_x",
+    "squares_exclude",
+    "squares_intersect",
+    "squares_subtract",
+    "squares_unite",
     "squircle",
     "squirrel",
     "stamp",
@@ -1556,6 +1593,7 @@ LUCIDE_ICON_LIST = [
     "train_front_tunnel",
     "train_track",
     "tram_front",
+    "transgender",
     "trash",
     "trash_2",
     "tree_deciduous",
@@ -1572,6 +1610,7 @@ LUCIDE_ICON_LIST = [
     "triangle_right",
     "trophy",
     "truck",
+    "truck_electric",
     "turtle",
     "tv",
     "tv_2",
@@ -1599,6 +1638,7 @@ LUCIDE_ICON_LIST = [
     "user",
     "user_check",
     "user_cog",
+    "user_lock",
     "user_minus",
     "user_pen",
     "user_plus",
@@ -1621,6 +1661,8 @@ LUCIDE_ICON_LIST = [
     "vault",
     "vegan",
     "venetian_mask",
+    "venus",
+    "venus_and_mars",
     "vibrate",
     "vibrate_off",
     "video",
@@ -1658,6 +1700,7 @@ LUCIDE_ICON_LIST = [
     "wifi_high",
     "wifi_low",
     "wifi_off",
+    "wifi_pen",
     "wifi_zero",
     "wind",
     "wind_arrow_down",

+ 2 - 22
reflex/components/markdown/markdown.py

@@ -4,9 +4,10 @@ from __future__ import annotations
 
 import dataclasses
 import textwrap
+from collections.abc import Callable, Sequence
 from functools import lru_cache
 from hashlib import md5
-from typing import Any, Callable, Sequence
+from typing import Any
 
 from reflex.components.component import BaseComponent, Component, CustomComponent
 from reflex.components.tags.tag import Tag
@@ -191,27 +192,6 @@ class Markdown(Component):
             **props,
         )
 
-    def _get_all_custom_components(
-        self, seen: set[str] | None = None
-    ) -> set[CustomComponent]:
-        """Get all the custom components used by the component.
-
-        Args:
-            seen: The tags of the components that have already been seen.
-
-        Returns:
-            The set of custom components.
-        """
-        custom_components = super()._get_all_custom_components(seen=seen)
-
-        # Get the custom components for each tag.
-        for component in self.component_map.values():
-            custom_components |= component(_MOCK_ARG)._get_all_custom_components(
-                seen=seen
-            )
-
-        return custom_components
-
     def add_imports(self) -> ImportDict | list[ImportDict]:
         """Add imports for the markdown component.
 

+ 1 - 1
reflex/components/moment/moment.py

@@ -29,7 +29,7 @@ class Moment(NoSSRComponent):
 
     tag: str | None = "Moment"
     is_default = True
-    library: str | None = "react-moment"
+    library: str | None = "react-moment@1.1.3"
     lib_dependencies: list[str] = ["moment"]
 
     # How often the date update (how often time update / 0 to disable).

+ 8 - 1
reflex/components/next/video.py

@@ -1,6 +1,7 @@
 """Wrapping of the next-video component."""
 
 from reflex.components.component import Component
+from reflex.utils import console
 from reflex.vars.base import Var
 
 from .base import NextComponent
@@ -10,7 +11,7 @@ class Video(NextComponent):
     """A video component from NextJS."""
 
     tag = "Video"
-    library = "next-video"
+    library = "next-video@2.2.0"
     is_default = True
     # the URL
     src: Var[str]
@@ -28,4 +29,10 @@ class Video(NextComponent):
         Returns:
             The Video component.
         """
+        console.deprecate(
+            "next-video",
+            "The next-video component is deprecated. Use `rx.video` instead.",
+            deprecation_version="0.7.11",
+            removal_version="0.8.0",
+        )
         return super().create(*children, **props)

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

@@ -2,7 +2,7 @@
 
 from __future__ import annotations
 
-from typing import Any, Dict, TypedDict, TypeVar
+from typing import Any, TypedDict, TypeVar
 
 from reflex.components.component import Component, NoSSRComponent
 from reflex.components.core.cond import color_mode_cond
@@ -81,13 +81,13 @@ class Plotly(NoSSRComponent):
     data: Var[Figure]  # pyright: ignore [reportInvalidTypeForm]
 
     # The layout of the graph.
-    layout: Var[Dict]
+    layout: Var[dict]
 
     # The template for visual appearance of the graph.
     template: Var[Template]  # pyright: ignore [reportInvalidTypeForm]
 
     # The config of the graph.
-    config: Var[Dict]
+    config: Var[dict]
 
     # If true, the graph will resize when the window is resized.
     use_resize_handler: Var[bool] = LiteralVar.create(True)

+ 3 - 2
reflex/components/radix/primitives/accordion.py

@@ -2,7 +2,8 @@
 
 from __future__ import annotations
 
-from typing import Any, ClassVar, Literal, Sequence
+from collections.abc import Sequence
+from typing import Any, ClassVar, Literal
 
 from reflex.components.component import Component, ComponentNamespace
 from reflex.components.core.colors import color
@@ -53,7 +54,7 @@ def _inherited_variant_selector(
 class AccordionComponent(RadixPrimitiveComponent):
     """Base class for all @radix-ui/accordion components."""
 
-    library = "@radix-ui/react-accordion@^1.2.3"
+    library = "@radix-ui/react-accordion@^1.2.8"
 
     # The color scheme of the component.
     color_scheme: Var[LiteralAccentColor]

+ 3 - 2
reflex/components/radix/primitives/drawer.py

@@ -4,7 +4,8 @@
 # Style based on https://ui.shadcn.com/docs/components/drawer
 from __future__ import annotations
 
-from typing import Any, Literal, Sequence
+from collections.abc import Sequence
+from typing import Any, Literal
 
 from reflex.components.component import Component, ComponentNamespace
 from reflex.components.radix.primitives.base import RadixPrimitiveComponent
@@ -18,7 +19,7 @@ from reflex.vars.base import Var
 class DrawerComponent(RadixPrimitiveComponent):
     """A Drawer component."""
 
-    library = "vaul"
+    library = "vaul@1.1.2"
 
     lib_dependencies: list[str] = ["@radix-ui/react-dialog@^1.1.6"]
 

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

@@ -17,7 +17,7 @@ from .base import RadixPrimitiveComponentWithClassName
 class FormComponent(RadixPrimitiveComponentWithClassName):
     """Base class for all @radix-ui/react-form components."""
 
-    library = "@radix-ui/react-form@^0.1.2"
+    library = "@radix-ui/react-form@^0.1.4"
 
 
 class FormRoot(FormComponent, HTMLForm):

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

@@ -15,7 +15,7 @@ from reflex.vars.base import Var
 class ProgressComponent(RadixPrimitiveComponentWithClassName):
     """A Progress component."""
 
-    library = "@radix-ui/react-progress@^1.1.2"
+    library = "@radix-ui/react-progress@^1.1.4"
 
 
 class ProgressRoot(ProgressComponent):

+ 3 - 2
reflex/components/radix/primitives/slider.py

@@ -2,7 +2,8 @@
 
 from __future__ import annotations
 
-from typing import Any, Literal, Sequence
+from collections.abc import Sequence
+from typing import Any, Literal
 
 from reflex.components.component import Component, ComponentNamespace
 from reflex.components.radix.primitives.base import RadixPrimitiveComponentWithClassName
@@ -16,7 +17,7 @@ LiteralSliderDir = Literal["ltr", "rtl"]
 class SliderComponent(RadixPrimitiveComponentWithClassName):
     """Base class for all @radix-ui/react-slider components."""
 
-    library = "@radix-ui/react-slider@^1.2.3"
+    library = "@radix-ui/react-slider@^1.3.2"
 
 
 def on_value_event_spec(

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

@@ -205,5 +205,5 @@ class ColorModeNamespace(Var):
 color_mode = color_mode_var_and_namespace = ColorModeNamespace(
     _js_expr=color_mode._js_expr,
     _var_type=color_mode._var_type,
-    _var_data=color_mode._get_default_value(),
+    _var_data=color_mode._get_all_var_data(),
 )

+ 2 - 1
reflex/components/radix/themes/components/checkbox_group.py

@@ -1,7 +1,8 @@
 """Components for the CheckboxGroup component of Radix Themes."""
 
+from collections.abc import Sequence
 from types import SimpleNamespace
-from typing import Literal, Sequence
+from typing import Literal
 
 from reflex.components.core.breakpoints import Responsive
 from reflex.vars.base import Var

+ 2 - 1
reflex/components/radix/themes/components/radio_group.py

@@ -2,7 +2,8 @@
 
 from __future__ import annotations
 
-from typing import Literal, Sequence
+from collections.abc import Sequence
+from typing import Literal
 
 import reflex as rx
 from reflex.components.component import Component, ComponentNamespace

+ 2 - 1
reflex/components/radix/themes/components/segmented_control.py

@@ -2,8 +2,9 @@
 
 from __future__ import annotations
 
+from collections.abc import Sequence
 from types import SimpleNamespace
-from typing import ClassVar, Literal, Sequence
+from typing import ClassVar, Literal
 
 from reflex.components.core.breakpoints import Responsive
 from reflex.event import EventHandler

+ 2 - 1
reflex/components/radix/themes/components/select.py

@@ -1,6 +1,7 @@
 """Interactive components provided by @radix-ui/themes."""
 
-from typing import ClassVar, Literal, Sequence
+from collections.abc import Sequence
+from typing import ClassVar, Literal
 
 import reflex as rx
 from reflex.components.component import Component, ComponentNamespace

+ 2 - 1
reflex/components/radix/themes/components/slider.py

@@ -2,7 +2,8 @@
 
 from __future__ import annotations
 
-from typing import Literal, Sequence
+from collections.abc import Sequence
+from typing import Literal
 
 from reflex.components.component import Component
 from reflex.components.core.breakpoints import Responsive

+ 2 - 2
reflex/components/radix/themes/components/tooltip.py

@@ -1,6 +1,6 @@
 """Interactive components provided by @radix-ui/themes."""
 
-from typing import Literal, Union
+from typing import Literal
 
 from reflex.components.component import Component
 from reflex.constants.compiler import MemoizationMode
@@ -63,7 +63,7 @@ class Tooltip(RadixThemesComponent):
     avoid_collisions: Var[bool]
 
     # The distance in pixels from the boundary edges where collision detection should occur. Accepts a number (same for all sides), or a partial padding object, for example: { "top": 20, "left": 20 }. Defaults to 0.
-    collision_padding: Var[Union[float, int, dict[str, float | int]]]
+    collision_padding: Var[float | int | dict[str, float | int]]
 
     # The padding between the arrow and the edges of the content. If your content has border-radius, this will prevent it from overflowing the corners. Defaults to 0.
     arrow_padding: Var[float | int]

+ 5 - 3
reflex/components/radix/themes/layout/list.py

@@ -2,10 +2,12 @@
 
 from __future__ import annotations
 
-from typing import Any, Iterable, Literal
+from collections.abc import Iterable
+from typing import Any, Literal
 
-from reflex.components.component import Component, ComponentNamespace
+from reflex.components.component import ComponentNamespace
 from reflex.components.core.foreach import Foreach
+from reflex.components.el.elements.base import BaseHTML
 from reflex.components.el.elements.typography import Li, Ol, Ul
 from reflex.components.lucide.icon import Icon
 from reflex.components.markdown.markdown import MarkdownComponentMap
@@ -37,7 +39,7 @@ LiteralListStyleTypeOrdered = Literal[
 ]
 
 
-class BaseList(Component, MarkdownComponentMap):
+class BaseList(BaseHTML, MarkdownComponentMap):
     """Base class for ordered and unordered lists."""
 
     tag = "ul"

+ 21 - 7
reflex/components/react_player/react_player.py

@@ -2,7 +2,7 @@
 
 from __future__ import annotations
 
-from typing import TypedDict
+from typing import Any, TypedDict
 
 from reflex.components.component import NoSSRComponent
 from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec
@@ -50,12 +50,6 @@ class ReactPlayer(NoSSRComponent):
     # Mutes the player
     muted: Var[bool]
 
-    # Set the width of the player: ex:640px
-    width: Var[str]
-
-    # Set the height of the player: ex:640px
-    height: Var[str]
-
     # Called when media is loaded and ready to play. If playing is set to true, media will play immediately.
     on_ready: EventHandler[no_args_event_spec]
 
@@ -103,3 +97,23 @@ class ReactPlayer(NoSSRComponent):
 
     # Called when picture-in-picture mode is disabled.
     on_disable_pip: EventHandler[no_args_event_spec]
+
+    def _render(self, props: dict[str, Any] | None = None):
+        """Render the component. Adds width and height set to None because
+        react-player will set them to some random value that overrides the
+        css width and height.
+
+        Args:
+            props: The props to pass to the component.
+
+        Returns:
+            The rendered component.
+        """
+        return (
+            super()
+            ._render(props)
+            .add_props(
+                width=Var.create(None),
+                height=Var.create(None),
+            )
+        )

+ 7 - 6
reflex/components/recharts/cartesian.py

@@ -2,7 +2,8 @@
 
 from __future__ import annotations
 
-from typing import Any, ClassVar, Sequence, Union
+from collections.abc import Sequence
+from typing import Any, ClassVar
 
 from reflex.constants import EventTriggers
 from reflex.constants.colors import Color
@@ -73,7 +74,7 @@ class Axis(Recharts):
     reversed: Var[bool]
 
     # The label of axis, which appears next to the axis.
-    label: Var[Union[str, int, dict[str, Any]]]
+    label: Var[str | int | dict[str, Any]]
 
     # If 'auto' set, the scale function is decided by the type of chart, and the props type. 'auto' | 'linear' | 'pow' | 'sqrt' | 'log' | 'identity' | 'time' | 'band' | 'point' | 'ordinal' | 'quantile' | 'quantize' | 'utc' | 'sequential' | 'threshold'. Default: "auto"
     scale: Var[LiteralScale]
@@ -343,10 +344,10 @@ class Area(Cartesian):
     type_: Var[LiteralAreaType] = LiteralVar.create("monotone")
 
     # If false set, dots will not be drawn. If true set, dots will be drawn which have the props calculated internally. Default: False
-    dot: Var[Union[bool, dict[str, Any]]]
+    dot: Var[bool | dict[str, Any]]
 
     # The dot is shown when user enter an area chart and this chart has tooltip. If false set, no active dot will not be drawn. If true set, active dot will be drawn which have the props calculated internally. Default: {stroke: rx.color("accent", 2), fill: rx.color("accent", 10)}
-    active_dot: Var[Union[bool, dict[str, Any]]] = LiteralVar.create(
+    active_dot: Var[bool | dict[str, Any]] = LiteralVar.create(
         {
             "stroke": Color("accent", 2),
             "fill": Color("accent", 10),
@@ -439,7 +440,7 @@ class Line(Cartesian):
     stroke_width: Var[int]
 
     # The dot is shown when mouse enter a line chart and this chart has tooltip. If false set, no active dot will not be drawn. If true set, active dot will be drawn which have the props calculated internally. Default: {"stroke": rx.color("accent", 10), "fill": rx.color("accent", 4)}
-    dot: Var[Union[bool, dict[str, Any]]] = LiteralVar.create(
+    dot: Var[bool | dict[str, Any]] = LiteralVar.create(
         {
             "stroke": Color("accent", 10),
             "fill": Color("accent", 4),
@@ -447,7 +448,7 @@ class Line(Cartesian):
     )
 
     # The dot is shown when user enter an area chart and this chart has tooltip. If false set, no active dot will not be drawn. If true set, active dot will be drawn which have the props calculated internally. Default: {"stroke": rx.color("accent", 2), "fill": rx.color("accent", 10)}
-    active_dot: Var[Union[bool, dict[str, Any]]] = LiteralVar.create(
+    active_dot: Var[bool | dict[str, Any]] = LiteralVar.create(
         {
             "stroke": Color("accent", 2),
             "fill": Color("accent", 10),

+ 2 - 1
reflex/components/recharts/charts.py

@@ -2,7 +2,8 @@
 
 from __future__ import annotations
 
-from typing import Any, ClassVar, Sequence
+from collections.abc import Sequence
+from typing import Any, ClassVar
 
 from reflex.components.component import Component
 from reflex.components.recharts.general import ResponsiveContainer

+ 3 - 2
reflex/components/recharts/general.py

@@ -2,7 +2,8 @@
 
 from __future__ import annotations
 
-from typing import Any, ClassVar, Sequence, Union
+from collections.abc import Sequence
+from typing import Any, ClassVar
 
 from reflex.components.component import MemoizationLeaf
 from reflex.constants.colors import Color
@@ -146,7 +147,7 @@ class GraphingTooltip(Recharts):
     filter_null: Var[bool]
 
     # If set false, no cursor will be drawn when tooltip is active. Default: {"strokeWidth": 1, "fill": rx.color("gray", 3)}
-    cursor: Var[Union[dict[str, Any], bool]] = LiteralVar.create(
+    cursor: Var[dict[str, Any] | bool] = LiteralVar.create(
         {
             "strokeWidth": 1,
             "fill": Color("gray", 3),

+ 9 - 8
reflex/components/recharts/polar.py

@@ -2,7 +2,8 @@
 
 from __future__ import annotations
 
-from typing import Any, ClassVar, Sequence, Union
+from collections.abc import Sequence
+from typing import Any, ClassVar
 
 from reflex.constants import EventTriggers
 from reflex.constants.colors import Color
@@ -189,10 +190,10 @@ class RadialBar(Recharts):
     legend_type: Var[LiteralLegendType]
 
     # If false set, labels will not be drawn. If true set, labels will be drawn which have the props calculated internally. Default: False
-    label: Var[Union[bool, dict[str, Any]]]
+    label: Var[bool | dict[str, Any]]
 
     # If false set, background sector will not be drawn. Default: False
-    background: Var[Union[bool, dict[str, Any]]]
+    background: Var[bool | dict[str, Any]]
 
     # If set false, animation of radial bars will be disabled. Default: True
     is_animation_active: Var[bool]
@@ -247,16 +248,16 @@ class PolarAngleAxis(Recharts):
     radius: Var[int | str]
 
     # 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
-    axis_line: Var[Union[bool, dict[str, Any]]]
+    axis_line: Var[bool | dict[str, Any]]
 
     # The type of axis line. Default: "polygon"
     axis_line_type: Var[LiteralGridType]
 
     # If false set, tick lines will not be drawn. If true set, tick lines will be drawn which have the props calculated internally. If object set, tick lines will be drawn which have the props mergered by the internal calculated props and the option. Default: False
-    tick_line: Var[Union[bool, dict[str, Any]]] = LiteralVar.create(False)
+    tick_line: Var[bool | dict[str, Any]] = LiteralVar.create(False)
 
     # 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: Var[Union[bool, dict[str, Any]]]
+    tick: Var[bool | dict[str, Any]]
 
     # The array of every tick's value and angle.
     ticks: Var[Sequence[dict[str, Any]]]
@@ -362,10 +363,10 @@ class PolarRadiusAxis(Recharts):
     orientation: Var[LiteralOrientationLeftRightMiddle]
 
     # 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
-    axis_line: Var[Union[bool, dict[str, Any]]]
+    axis_line: Var[bool | dict[str, Any]]
 
     # 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: Var[Union[bool, dict[str, Any]]]
+    tick: Var[bool | dict[str, Any]]
 
     # The count of axis ticks. Not used if 'type' is 'category'. Default: 5
     tick_count: Var[int]

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

@@ -8,7 +8,7 @@ from reflex.components.component import Component, MemoizationLeaf, NoSSRCompone
 class Recharts(Component):
     """A component that wraps a recharts lib."""
 
-    library = "recharts@2.15.1"
+    library = "recharts@2.15.3"
 
     def _get_style(self) -> dict:
         return {"wrapperStyle": self.style}
@@ -17,7 +17,7 @@ class Recharts(Component):
 class RechartsCharts(NoSSRComponent, MemoizationLeaf):
     """A component that wraps a recharts lib."""
 
-    library = "recharts@2.15.1"
+    library = "recharts@2.15.3"
 
 
 LiteralAnimationEasing = Literal["ease", "ease-in", "ease-out", "ease-in-out", "linear"]

+ 1 - 1
reflex/components/sonner/toast.py

@@ -172,7 +172,7 @@ class ToastProps(PropsBase, NoExtrasAllowedProps):
 class Toaster(Component):
     """A Toaster Component for displaying toast notifications."""
 
-    library: str | None = "sonner@2.0.1"
+    library: str | None = "sonner@2.0.3"
 
     tag = "Toaster"
 

+ 23 - 25
reflex/components/suneditor/editor.py

@@ -3,7 +3,7 @@
 from __future__ import annotations
 
 import enum
-from typing import Any, Dict, Literal, Union
+from typing import Any, Literal
 
 from reflex.base import Base
 from reflex.components.component import Component, NoSSRComponent
@@ -103,7 +103,7 @@ class Editor(NoSSRComponent):
     refer to the library docs for a complete list.
     """
 
-    library = "suneditor-react"
+    library = "suneditor-react@3.6.1"
 
     tag = "SunEditor"
 
@@ -118,29 +118,27 @@ class Editor(NoSSRComponent):
     # "ru" | "zh_cn" | "ro" | "pl" | "ckb" | "lv" | "se" | "ua" | "he" | "it"
     # default: "en".
     lang: Var[
-        Union[
-            Literal[
-                "en",
-                "da",
-                "de",
-                "es",
-                "fr",
-                "ja",
-                "ko",
-                "pt_br",
-                "ru",
-                "zh_cn",
-                "ro",
-                "pl",
-                "ckb",
-                "lv",
-                "se",
-                "ua",
-                "he",
-                "it",
-            ],
-            dict,
+        Literal[
+            "en",
+            "da",
+            "de",
+            "es",
+            "fr",
+            "ja",
+            "ko",
+            "pt_br",
+            "ru",
+            "zh_cn",
+            "ro",
+            "pl",
+            "ckb",
+            "lv",
+            "se",
+            "ua",
+            "he",
+            "it",
         ]
+        | dict
     ]
 
     # This is used to set the HTML form name of the editor.
@@ -169,7 +167,7 @@ class Editor(NoSSRComponent):
     auto_focus: Var[bool]
 
     # Pass an EditorOptions instance to modify the behaviour of Editor even more.
-    set_options: Var[Dict]
+    set_options: Var[dict]
 
     # Whether all SunEditor plugins should be loaded.
     # default: True.

+ 3 - 3
reflex/components/tags/cond_tag.py

@@ -1,7 +1,7 @@
 """Tag to conditionally render components."""
 
 import dataclasses
-from typing import Any, Dict
+from typing import Any
 
 from reflex.components.tags.tag import Tag
 from reflex.vars.base import Var
@@ -15,7 +15,7 @@ class CondTag(Tag):
     cond: Var[Any] = dataclasses.field(default_factory=lambda: Var.create(True))
 
     # The code to render if the condition is true.
-    true_value: Dict = dataclasses.field(default_factory=dict)
+    true_value: dict = dataclasses.field(default_factory=dict)
 
     # The code to render if the condition is false.
-    false_value: Dict | None = None
+    false_value: dict | None = None

+ 2 - 1
reflex/components/tags/iter_tag.py

@@ -4,7 +4,8 @@ from __future__ import annotations
 
 import dataclasses
 import inspect
-from typing import TYPE_CHECKING, Callable, Iterable
+from collections.abc import Callable, Iterable
+from typing import TYPE_CHECKING
 
 from reflex.components.tags.tag import Tag
 from reflex.utils.types import GenericType

+ 3 - 2
reflex/components/tags/tag.py

@@ -3,7 +3,8 @@
 from __future__ import annotations
 
 import dataclasses
-from typing import Any, List, Mapping, Sequence
+from collections.abc import Mapping, Sequence
+from typing import Any
 
 from reflex.event import EventChain
 from reflex.utils import format
@@ -57,7 +58,7 @@ class Tag:
             {name: LiteralVar.create(value) for name, value in self.props.items()},
         )
 
-    def format_props(self) -> List:
+    def format_props(self) -> list:
         """Format the tag's props.
 
         Returns:

+ 34 - 24
reflex/config.py

@@ -13,6 +13,7 @@ import platform
 import sys
 import threading
 import urllib.parse
+from collections.abc import Callable
 from functools import lru_cache
 from importlib.util import find_spec
 from pathlib import Path
@@ -21,7 +22,6 @@ from typing import (
     TYPE_CHECKING,
     Annotated,
     Any,
-    Callable,
     Generic,
     TypeVar,
     get_args,
@@ -49,6 +49,29 @@ except ImportError:
     load_dotenv = None
 
 
+def _load_dotenv_from_str(env_files: str) -> None:
+    if not env_files:
+        return
+
+    if load_dotenv is None:
+        console.error(
+            """The `python-dotenv` package is required to load environment variables from a file. Run `pip install "python-dotenv>=1.1.0"`."""
+        )
+        return
+
+    # load env files in reverse order if they exist
+    for env_file_path in [
+        Path(p) for s in reversed(env_files.split(os.pathsep)) if (p := s.strip())
+    ]:
+        if env_file_path.exists():
+            load_dotenv(env_file_path, override=True)
+
+
+# Load the env files at import time if they are set in the ENV_FILE environment variable.
+if env_files := os.getenv("ENV_FILE"):
+    _load_dotenv_from_str(env_files)
+
+
 class DBConfig(Base):
     """Database config."""
 
@@ -369,7 +392,7 @@ class EnvVar(Generic[T]):
             The environment variable value.
         """
         env_value = os.getenv(self.name, None)
-        if env_value is not None:
+        if env_value and env_value.strip():
             return self.interpret(env_value)
         return None
 
@@ -379,7 +402,7 @@ class EnvVar(Generic[T]):
         Returns:
             True if the environment variable is set.
         """
-        return self.name in os.environ
+        return bool(os.getenv(self.name, "").strip())
 
     def get(self) -> T:
         """Get the interpreted environment variable value or the default value if not set.
@@ -410,7 +433,7 @@ class EnvVar(Generic[T]):
             os.environ[self.name] = str_value
 
 
-@lru_cache()
+@lru_cache
 def get_type_hints_environment(cls: type) -> dict[str, Any]:
     """Get the type hints for the environment variables.
 
@@ -672,9 +695,6 @@ class EnvironmentVariables:
     # The port to run the backend on.
     REFLEX_BACKEND_PORT: EnvVar[int | None] = env_var(None)
 
-    # Reflex internal env to reload the config.
-    RELOAD_CONFIG: EnvVar[bool] = env_var(False, internal=True)
-
     # If this env var is set to "yes", App.compile will be a no-op
     REFLEX_SKIP_COMPILE: EnvVar[bool] = env_var(False, internal=True)
 
@@ -859,6 +879,9 @@ class Config(Base):
     # Path to file containing key-values pairs to override in the environment; Dotenv format.
     env_file: str | None = None
 
+    # Whether to automatically create setters for state base vars
+    state_auto_setters: bool = True
+
     # Whether to display the sticky "Built with Reflex" badge on all pages.
     show_built_with_reflex: bool | None = None
 
@@ -883,7 +906,7 @@ class Config(Base):
         # Set the log level for this process
         env_loglevel = os.environ.get("LOGLEVEL")
         if env_loglevel is not None:
-            env_loglevel = LogLevel(env_loglevel)
+            env_loglevel = LogLevel(env_loglevel.lower())
         if env_loglevel or self.loglevel != LogLevel.DEFAULT:
             console.set_log_level(env_loglevel or self.loglevel)
 
@@ -936,21 +959,8 @@ class Config(Base):
         Returns:
             The updated config values.
         """
-        env_file = self.env_file or os.environ.get("ENV_FILE", None)
-        if env_file:
-            if load_dotenv is None:
-                console.error(
-                    """The `python-dotenv` package is required to load environment variables from a file. Run `pip install "python-dotenv>=1.0.1"`."""
-                )
-            else:
-                # load env files in reverse order if they exist
-                for env_file_path in [
-                    Path(p)
-                    for s in reversed(env_file.split(os.pathsep))
-                    if (p := s.strip())
-                ]:
-                    if env_file_path.exists():
-                        load_dotenv(env_file_path, override=True)
+        if self.env_file:
+            _load_dotenv_from_str(self.env_file)
 
         updated_values = {}
         # Iterate over the fields.
@@ -959,7 +969,7 @@ class Config(Base):
             env_var = os.environ.get(key.upper())
 
             # If the env var is set, override the config value.
-            if env_var is not None:
+            if env_var and env_var.strip():
                 # Interpret the value.
                 value = interpret_env_var_value(
                     env_var, true_type_for_pydantic_field(field), field.name

+ 2 - 0
reflex/constants/__init__.py

@@ -41,6 +41,7 @@ from .config import (
     DefaultPorts,
     Expiration,
     GitIgnore,
+    PyprojectToml,
     RequirementsTxt,
 )
 from .custom_components import CustomComponents
@@ -106,6 +107,7 @@ __all__ = [
     "Page404",
     "PageNames",
     "Ping",
+    "PyprojectToml",
     "Reflex",
     "RequirementsTxt",
     "RouteArgType",

+ 22 - 1
reflex/constants/base.py

@@ -7,6 +7,7 @@ from enum import Enum
 from importlib import metadata
 from pathlib import Path
 from types import SimpleNamespace
+from typing import Literal
 
 from platformdirs import PlatformDirs
 
@@ -135,7 +136,7 @@ class Templates(SimpleNamespace):
     DEFAULT_TEMPLATE_URL = "https://blank-template.reflex.run"
 
     # The reflex.build frontend host
-    REFLEX_BUILD_FRONTEND = "https://flexgen.reflex.run"
+    REFLEX_BUILD_FRONTEND = "https://reflex.build"
 
     # The reflex.build backend host
     REFLEX_BUILD_BACKEND = "https://flexgen-prod-flexgen.fly.dev"
@@ -221,6 +222,9 @@ class ColorMode(SimpleNamespace):
     SET = "setColorMode"
 
 
+LITERAL_ENV = Literal["dev", "prod"]
+
+
 # Env modes
 class Env(str, Enum):
     """The environment modes."""
@@ -240,6 +244,23 @@ class LogLevel(str, Enum):
     ERROR = "error"
     CRITICAL = "critical"
 
+    @classmethod
+    def from_string(cls, level: str | None) -> LogLevel | None:
+        """Convert a string to a log level.
+
+        Args:
+            level: The log level as a string.
+
+        Returns:
+            The log level.
+        """
+        if not level:
+            return None
+        try:
+            return LogLevel[level.upper()]
+        except KeyError:
+            return None
+
     def __le__(self, other: LogLevel) -> bool:
         """Compare log levels.
 

+ 23 - 6
reflex/constants/colors.py

@@ -1,7 +1,12 @@
 """The colors used in Reflex are a wrapper around https://www.radix-ui.com/colors."""
 
+from __future__ import annotations
+
 from dataclasses import dataclass
-from typing import Literal
+from typing import TYPE_CHECKING, Literal, get_args
+
+if TYPE_CHECKING:
+    from reflex.vars import Var
 
 ColorType = Literal[
     "gray",
@@ -40,10 +45,16 @@ ColorType = Literal[
     "white",
 ]
 
+COLORS = frozenset(get_args(ColorType))
+
 ShadeType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
+MIN_SHADE_VALUE = 1
+MAX_SHADE_VALUE = 12
 
 
-def format_color(color: ColorType, shade: ShadeType, alpha: bool) -> str:
+def format_color(
+    color: ColorType | Var[str], shade: ShadeType | Var[int], alpha: bool | Var[bool]
+) -> str:
     """Format a color as a CSS color string.
 
     Args:
@@ -54,7 +65,13 @@ def format_color(color: ColorType, shade: ShadeType, alpha: bool) -> str:
     Returns:
         The formatted color.
     """
-    return f"var(--{color}-{'a' if alpha else ''}{shade})"
+    if isinstance(alpha, bool):
+        return f"var(--{color}-{'a' if alpha else ''}{shade})"
+
+    from reflex.components.core import cond
+
+    alpha_var = cond(alpha, "a", "")
+    return f"var(--{color}-{alpha_var}{shade})"
 
 
 @dataclass
@@ -62,13 +79,13 @@ class Color:
     """A color in the Reflex color palette."""
 
     # The color palette to use
-    color: ColorType
+    color: ColorType | Var[str]
 
     # The shade of the color to use
-    shade: ShadeType = 7
+    shade: ShadeType | Var[int] = 7
 
     # Whether to use the alpha variant of the color
-    alpha: bool = False
+    alpha: bool | Var[bool] = False
 
     def __format__(self, format_spec: str) -> str:
         """Format the color as a CSS color string.

+ 7 - 0
reflex/constants/config.py

@@ -49,6 +49,13 @@ class GitIgnore(SimpleNamespace):
     }
 
 
+class PyprojectToml(SimpleNamespace):
+    """Pyproject.toml constants."""
+
+    # The pyproject.toml file.
+    FILE = "pyproject.toml"
+
+
 class RequirementsTxt(SimpleNamespace):
     """Requirements.txt constants."""
 

+ 20 - 6
reflex/constants/installer.py

@@ -14,7 +14,7 @@ class Bun(SimpleNamespace):
     """Bun constants."""
 
     # The Bun version.
-    VERSION = "1.2.8"
+    VERSION = "1.2.12"
 
     # Min Bun Version
     MIN_VERSION = "1.2.8"
@@ -75,7 +75,7 @@ fetch-retries=0
 
 
 def _determine_nextjs_version() -> str:
-    default_version = "15.2.4"
+    default_version = "15.3.2"
     if (version := os.getenv("NEXTJS_VERSION")) and version != default_version:
         from reflex.utils import console
 
@@ -86,6 +86,18 @@ def _determine_nextjs_version() -> str:
     return default_version
 
 
+def _determine_react_version() -> str:
+    default_version = "19.1.0"
+    if (version := os.getenv("REACT_VERSION")) and version != default_version:
+        from reflex.utils import console
+
+        console.warn(
+            f"You have requested react@{version} but the supported version is {default_version}, abandon all hope ye who enter here."
+        )
+        return version
+    return default_version
+
+
 class PackageJson(SimpleNamespace):
     """Constants used to build the package.json file."""
 
@@ -99,16 +111,18 @@ class PackageJson(SimpleNamespace):
 
     PATH = "package.json"
 
+    _react_version = _determine_react_version()
+
     DEPENDENCIES = {
-        "axios": "1.8.3",
+        "axios": "1.9.0",
         "json5": "2.2.3",
         "react-router": "7.5.0",
         "react-router-dom": "7.5.0",
         "react-helmet": "6.1.0",
         "@react-router/node": "7.5.0",
         "serve": "14.2.4",
-        "react": "19.1.0",
-        "react-dom": "19.1.0",
+        "react": _react_version,
+        "react-dom": _react_version,
         "isbot": "5.1.26",
         "socket.io-client": "4.8.1",
         "universal-cookie": "7.2.2",
@@ -125,6 +139,6 @@ class PackageJson(SimpleNamespace):
     }
     OVERRIDES = {
         # This should always match the `react` version in DEPENDENCIES for recharts compatibility.
-        "react-is": "19.0.0",
+        "react-is": _react_version,
         "cookie": "1.0.2",
     }

+ 2 - 3
reflex/constants/route.py

@@ -7,9 +7,8 @@ from types import SimpleNamespace
 class RouteArgType(SimpleNamespace):
     """Type of dynamic route arg extracted from URI route."""
 
-    # Typecast to str is needed for Enum to work.
-    SINGLE = str("arg_single")
-    LIST = str("arg_list")
+    SINGLE = "arg_single"
+    LIST = "arg_list"
 
 
 # the name of the backend var containing path and client information

+ 4 - 3
reflex/constants/utils.py

@@ -1,6 +1,7 @@
 """Utility functions for constants."""
 
-from typing import Any, Callable, Generic, Type, TypeVar
+from collections.abc import Callable
+from typing import Any, Generic, TypeVar
 
 T = TypeVar("T")
 V = TypeVar("V")
@@ -9,7 +10,7 @@ V = TypeVar("V")
 class classproperty(Generic[T, V]):
     """A class property decorator."""
 
-    def __init__(self, getter: Callable[[Type[T]], V]) -> None:
+    def __init__(self, getter: Callable[[type[T]], V]) -> None:
         """Initialize the class property.
 
         Args:
@@ -17,7 +18,7 @@ class classproperty(Generic[T, V]):
         """
         self.getter = getattr(getter, "__func__", getter)
 
-    def __get__(self, instance: Any, owner: Type[T]) -> V:
+    def __get__(self, instance: Any, owner: type[T]) -> V:
         """Get the value of the class property.
 
         Args:

+ 67 - 64
reflex/custom_components/custom_components.py

@@ -9,17 +9,46 @@ import sys
 from collections import namedtuple
 from contextlib import contextmanager
 from pathlib import Path
+from typing import Any
 
+import click
 import httpx
-import typer
 
 from reflex import constants
-from reflex.config import get_config
 from reflex.constants import CustomComponents
 from reflex.utils import console
 
-custom_components_cli = typer.Typer()
 
+def set_loglevel(ctx: Any, self: Any, value: str | None):
+    """Set the log level.
+
+    Args:
+        ctx: The click context.
+        self: The click command.
+        value: The log level to set.
+    """
+    if value is not None:
+        loglevel = constants.LogLevel.from_string(value)
+        console.set_log_level(loglevel)
+
+
+@click.group
+def custom_components_cli():
+    """CLI for creating custom components."""
+    pass
+
+
+loglevel_option = click.option(
+    "--loglevel",
+    type=click.Choice(
+        [loglevel.value for loglevel in constants.LogLevel],
+        case_sensitive=False,
+    ),
+    callback=set_loglevel,
+    is_eager=True,
+    expose_value=False,
+    help="The log level to use.",
+)
 
 POST_CUSTOM_COMPONENTS_GALLERY_TIMEOUT = 15
 
@@ -163,13 +192,13 @@ def _get_default_library_name_parts() -> list[str]:
             console.error(
                 f"Based on current directory name {current_dir_name}, the library name is {constants.Reflex.MODULE_NAME}. This package already exists. Please use --library-name to specify a different name."
             )
-            raise typer.Exit(code=1)
+            raise click.exceptions.Exit(code=1)
     if not parts:
         # The folder likely has a name not suitable for python paths.
         console.error(
             f"Could not find a valid library name based on the current directory: got {current_dir_name}."
         )
-        raise typer.Exit(code=1)
+        raise click.exceptions.Exit(code=1)
     return parts
 
 
@@ -205,7 +234,7 @@ def _validate_library_name(library_name: str | None) -> NameVariants:
         console.error(
             f"Please use only alphanumeric characters or dashes: got {library_name}"
         )
-        raise typer.Exit(code=1)
+        raise click.exceptions.Exit(code=1)
 
     # If not specified, use the current directory name to form the module name.
     name_parts = (
@@ -277,36 +306,35 @@ def _populate_custom_component_project(name_variants: NameVariants):
 
 
 @custom_components_cli.command(name="init")
+@click.option(
+    "--library-name",
+    default=None,
+    help="The name of your library. On PyPI, package will be published as `reflex-{library-name}`.",
+)
+@click.option(
+    "--install/--no-install",
+    default=True,
+    help="Whether to install package from this local custom component in editable mode.",
+)
+@loglevel_option
 def init(
-    library_name: str | None = typer.Option(
-        None,
-        help="The name of your library. On PyPI, package will be published as `reflex-{library-name}`.",
-    ),
-    install: bool = typer.Option(
-        True,
-        help="Whether to install package from this local custom component in editable mode.",
-    ),
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
+    library_name: str | None,
+    install: bool,
 ):
     """Initialize a custom component.
 
     Args:
         library_name: The name of the library.
         install: Whether to install package from this local custom component in editable mode.
-        loglevel: The log level to use.
 
     Raises:
         Exit: If the pyproject.toml already exists.
     """
     from reflex.utils import exec, prerequisites
 
-    console.set_log_level(loglevel or get_config().loglevel)
-
     if CustomComponents.PYPROJECT_TOML.exists():
         console.error(f"A {CustomComponents.PYPROJECT_TOML} already exists. Aborting.")
-        typer.Exit(code=1)
+        click.exceptions.Exit(code=1)
 
     # Show system info.
     exec.output_system_info()
@@ -331,7 +359,7 @@ def init(
         if _pip_install_on_demand(package_name=".", install_args=["-e"]):
             console.info(f"Package {package_name} installed!")
         else:
-            raise typer.Exit(code=1)
+            raise click.exceptions.Exit(code=1)
 
     console.print("[bold]Custom component initialized successfully!")
     console.rule("[bold]Project Summary")
@@ -424,21 +452,13 @@ def _run_build():
     if _run_commands_in_subprocess(cmds):
         console.info("Custom component built successfully!")
     else:
-        raise typer.Exit(code=1)
+        raise click.exceptions.Exit(code=1)
 
 
 @custom_components_cli.command(name="build")
-def build(
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
-):
-    """Build a custom component. Must be run from the project root directory where the pyproject.toml is.
-
-    Args:
-        loglevel: The log level to use.
-    """
-    console.set_log_level(loglevel or get_config().loglevel)
+@loglevel_option
+def build():
+    """Build a custom component. Must be run from the project root directory where the pyproject.toml is."""
     _run_build()
 
 
@@ -453,7 +473,7 @@ def publish():
         "The publish command is deprecated. You can use `reflex component build` followed by `twine upload` or a similar publishing command to publish your custom component."
         "\nIf you want to share your custom component with the Reflex community, please use `reflex component share`."
     )
-    raise typer.Exit(code=1)
+    raise click.exceptions.Exit(code=1)
 
 
 def _collect_details_for_gallery():
@@ -472,7 +492,7 @@ def _collect_details_for_gallery():
         console.error(
             "Unable to authenticate with Reflex backend services. Make sure you are logged in."
         )
-        raise typer.Exit(code=1)
+        raise click.exceptions.Exit(code=1)
 
     console.rule("[bold]Custom Component Information")
     params = {}
@@ -502,11 +522,11 @@ def _collect_details_for_gallery():
             console.error(
                 f"{package_name} is owned by another user. Unable to update the information for it."
             )
-            raise typer.Exit(code=1)
+            raise click.exceptions.Exit(code=1)
         response.raise_for_status()
     except httpx.HTTPError as he:
         console.error(f"Unable to complete request due to {he}.")
-        raise typer.Exit(code=1) from he
+        raise click.exceptions.Exit(code=1) from he
 
     files = []
     if (image_file_and_extension := _get_file_from_prompt_in_loop()) is not None:
@@ -541,7 +561,7 @@ def _collect_details_for_gallery():
 
     except httpx.HTTPError as he:
         console.error(f"Unable to complete request due to {he}.")
-        raise typer.Exit(code=1) from he
+        raise click.exceptions.Exit(code=1) from he
 
     console.info("Custom component information successfully shared!")
 
@@ -577,7 +597,7 @@ def _get_file_from_prompt_in_loop() -> tuple[bytes, str] | None:
             image_file = image_file_path.read_bytes()
         except OSError as ose:
             console.error(f"Unable to read the {file_extension} file due to {ose}")
-            raise typer.Exit(code=1) from ose
+            raise click.exceptions.Exit(code=1) from ose
         else:
             return image_file, file_extension
 
@@ -586,38 +606,21 @@ def _get_file_from_prompt_in_loop() -> tuple[bytes, str] | None:
 
 
 @custom_components_cli.command(name="share")
-def share_more_detail(
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
-):
-    """Collect more details on the published package for gallery.
-
-    Args:
-        loglevel: The log level to use.
-    """
-    console.set_log_level(loglevel or get_config().loglevel)
-
+@loglevel_option
+def share_more_detail():
+    """Collect more details on the published package for gallery."""
     _collect_details_for_gallery()
 
 
-@custom_components_cli.command()
-def install(
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
-):
+@custom_components_cli.command(name="install")
+@loglevel_option
+def install():
     """Install package from this local custom component in editable mode.
 
-    Args:
-        loglevel: The log level to use.
-
     Raises:
         Exit: If unable to install the current directory in editable mode.
     """
-    console.set_log_level(loglevel or get_config().loglevel)
-
     if _pip_install_on_demand(package_name=".", install_args=["-e"]):
         console.info("Package installed successfully!")
     else:
-        raise typer.Exit(code=1)
+        raise click.exceptions.Exit(code=1)

+ 82 - 28
reflex/event.py

@@ -7,16 +7,14 @@ import inspect
 import types
 import urllib.parse
 from base64 import b64encode
+from collections.abc import Callable, Sequence
 from functools import partial
 from typing import (
     TYPE_CHECKING,
     Annotated,
     Any,
-    Callable,
     Generic,
     Protocol,
-    Sequence,
-    Type,
     TypedDict,
     TypeVar,
     get_args,
@@ -40,6 +38,7 @@ from reflex.utils.exceptions import (
 from reflex.utils.types import (
     ArgsSpec,
     GenericType,
+    Unset,
     safe_issubclass,
     typehint_issubclass,
 )
@@ -202,13 +201,14 @@ class EventHandler(EventActionsMixin):
         """
         return getattr(self.fn, BACKGROUND_TASK_MARKER, False)
 
-    def __call__(self, *args: Any) -> EventSpec:
+    def __call__(self, *args: Any, **kwargs: Any) -> EventSpec:
         """Pass arguments to the handler to get an event spec.
 
         This method configures event handlers that take in arguments.
 
         Args:
             *args: The arguments to pass to the handler.
+            **kwargs: The keyword arguments to pass to the handler.
 
         Returns:
             The event spec, containing both the function and args.
@@ -220,11 +220,34 @@ class EventHandler(EventActionsMixin):
 
         # Get the function args.
         fn_args = list(inspect.signature(self.fn).parameters)[1:]
+
+        if not isinstance(
+            repeated_arg := next(
+                (kwarg for kwarg in kwargs if kwarg in fn_args[: len(args)]), Unset()
+            ),
+            Unset,
+        ):
+            raise EventHandlerTypeError(
+                f"Event handler {self.fn.__name__} received repeated argument {repeated_arg}."
+            )
+
+        if not isinstance(
+            extra_arg := next(
+                (kwarg for kwarg in kwargs if kwarg not in fn_args), Unset()
+            ),
+            Unset,
+        ):
+            raise EventHandlerTypeError(
+                f"Event handler {self.fn.__name__} received extra argument {extra_arg}."
+            )
+
+        fn_args = fn_args[: len(args)] + list(kwargs)
+
         fn_args = (Var(_js_expr=arg) for arg in fn_args)
 
         # Construct the payload.
         values = []
-        for arg in args:
+        for arg in [*args, *kwargs.values()]:
             # Special case for file uploads.
             if isinstance(arg, FileUpload):
                 return arg.as_event_spec(handler=self)
@@ -645,21 +668,21 @@ class IdentityEventReturn(Generic[T], Protocol):
 
 @overload
 def passthrough_event_spec(  # pyright: ignore [reportOverlappingOverload]
-    event_type: Type[T], /
+    event_type: type[T], /
 ) -> Callable[[Var[T]], tuple[Var[T]]]: ...
 
 
 @overload
 def passthrough_event_spec(
-    event_type_1: Type[T], event_type2: Type[U], /
+    event_type_1: type[T], event_type2: type[U], /
 ) -> Callable[[Var[T], Var[U]], tuple[Var[T], Var[U]]]: ...
 
 
 @overload
-def passthrough_event_spec(*event_types: Type[T]) -> IdentityEventReturn[T]: ...
+def passthrough_event_spec(*event_types: type[T]) -> IdentityEventReturn[T]: ...
 
 
-def passthrough_event_spec(*event_types: Type[T]) -> IdentityEventReturn[T]:  # pyright: ignore [reportInconsistentOverload]
+def passthrough_event_spec(*event_types: type[T]) -> IdentityEventReturn[T]:  # pyright: ignore [reportInconsistentOverload]
     """A helper function that returns the input event as output.
 
     Args:
@@ -1132,10 +1155,12 @@ def call_script(
     callback_kwargs = {}
     if callback is not None:
         callback_kwargs = {
-            "callback": format.format_queue_events(
-                callback,
-                args_spec=lambda result: [result],
-            )._js_expr,
+            "callback": str(
+                format.format_queue_events(
+                    callback,
+                    args_spec=lambda result: [result],
+                )
+            ),
         }
     if isinstance(javascript_code, str):
         # When there is VarData, include it and eval the JS code inline on the client.
@@ -1171,10 +1196,12 @@ def call_function(
     callback_kwargs = {"callback": None}
     if callback is not None:
         callback_kwargs = {
-            "callback": format.format_queue_events(
-                callback,
-                args_spec=lambda result: [result],
-            ),
+            "callback": str(
+                format.format_queue_events(
+                    callback,
+                    args_spec=lambda result: [result],
+                ),
+            )
         }
 
     javascript_code = (
@@ -1779,7 +1806,7 @@ class LiteralEventChainVar(ArgsFunctionOperationBuilder, LiteralVar, EventChainV
         )
         sig = inspect.signature(arg_spec)  # pyright: ignore [reportArgumentType]
         if sig.parameters:
-            arg_def = tuple((f"_{p}" for p in sig.parameters))
+            arg_def = tuple(f"_{p}" for p in sig.parameters)
             arg_def_expr = LiteralVar.create([Var(_js_expr=arg) for arg in arg_def])
         else:
             # add a default argument for addEvents if none were specified in value.args_spec
@@ -1960,7 +1987,9 @@ IndividualEventType = TypeAliasType(
 )
 
 EventType = TypeAliasType(
-    "EventType", ItemOrList[IndividualEventType[Unpack[ARGS]]], type_params=(ARGS,)
+    "EventType",
+    ItemOrList[LAMBDA_OR_STATE[Unpack[ARGS]] | BASIC_EVENT_TYPES],
+    type_params=(ARGS,),
 )
 
 
@@ -1972,7 +2001,7 @@ else:
     BASE_STATE = TypeVar("BASE_STATE")
 
 
-class EventNamespace(types.SimpleNamespace):
+class EventNamespace:
     """A namespace for event related classes."""
 
     Event = Event
@@ -1988,23 +2017,22 @@ class EventNamespace(types.SimpleNamespace):
     EventCallback = EventCallback
 
     @overload
-    @staticmethod
-    def __call__(
-        func: None = None, *, background: bool | None = None
+    def __new__(
+        cls, func: None = None, *, background: bool | None = None
     ) -> Callable[
         [Callable[[BASE_STATE, Unpack[P]], Any]], EventCallback[Unpack[P]]  # pyright: ignore [reportInvalidTypeVarUse]
     ]: ...
 
     @overload
-    @staticmethod
-    def __call__(
+    def __new__(
+        cls,
         func: Callable[[BASE_STATE, Unpack[P]], Any],
         *,
         background: bool | None = None,
     ) -> EventCallback[Unpack[P]]: ...
 
-    @staticmethod
-    def __call__(
+    def __new__(
+        cls,
         func: Callable[[BASE_STATE, Unpack[P]], Any] | None = None,
         *,
         background: bool | None = None,
@@ -2036,6 +2064,32 @@ class EventNamespace(types.SimpleNamespace):
                         "Background task must be async function or generator."
                     )
                 setattr(func, BACKGROUND_TASK_MARKER, True)
+            if getattr(func, "__name__", "").startswith("_"):
+                raise ValueError("Event handlers cannot be private.")
+
+            qualname: str | None = getattr(func, "__qualname__", None)
+
+            if qualname and (
+                len(func_path := qualname.split(".")) == 1
+                or func_path[-2] == "<locals>"
+            ):
+                from reflex.state import BaseState
+
+                types = get_type_hints(func)
+                state_arg_name = next(iter(inspect.signature(func).parameters), None)
+                state_cls = state_arg_name and types.get(state_arg_name)
+                if state_cls and issubclass(state_cls, BaseState):
+                    name = (
+                        (func.__module__ + "." + qualname)
+                        .replace(".", "_")
+                        .replace("<locals>", "_")
+                        .removeprefix("_")
+                    )
+                    object.__setattr__(func, "__name__", name)
+                    object.__setattr__(func, "__qualname__", name)
+                    state_cls._add_event_handler(name, func)
+                    return getattr(state_cls, name)
+
             return func  # pyright: ignore [reportReturnType]
 
         if func is not None:
@@ -2076,4 +2130,4 @@ class EventNamespace(types.SimpleNamespace):
     run_script = staticmethod(run_script)
 
 
-event = EventNamespace()
+event = EventNamespace

+ 3 - 2
reflex/experimental/client_state.py

@@ -4,7 +4,8 @@ from __future__ import annotations
 
 import dataclasses
 import re
-from typing import Any, Callable
+from collections.abc import Callable
+from typing import Any
 
 from reflex import constants
 from reflex.event import EventChain, EventHandler, EventSpec, run_script
@@ -77,7 +78,7 @@ class ClientStateVar(Var):
         var_name: str | None = None,
         default: Any = NoValue,
         global_ref: bool = True,
-    ) -> "ClientStateVar":
+    ) -> ClientStateVar:
         """Create a local_state Var that can be accessed and updated on the client.
 
         The `ClientStateVar` should be included in the highest parent component

+ 1 - 1
reflex/istate/data.py

@@ -1,7 +1,7 @@
 """This module contains the dataclasses representing the router object."""
 
 import dataclasses
-from typing import Mapping
+from collections.abc import Mapping
 
 from reflex import constants
 from reflex.utils import format

+ 858 - 0
reflex/istate/manager.py

@@ -0,0 +1,858 @@
+"""State manager for managing client states."""
+
+import asyncio
+import contextlib
+import dataclasses
+import functools
+import time
+import uuid
+from abc import ABC, abstractmethod
+from collections.abc import AsyncIterator
+from hashlib import md5
+from pathlib import Path
+
+from redis import ResponseError
+from redis.asyncio import Redis
+from redis.asyncio.client import PubSub
+from typing_extensions import override
+
+from reflex import constants
+from reflex.config import environment, get_config
+from reflex.state import BaseState, _split_substate_key, _substate_key
+from reflex.utils import console, path_ops, prerequisites
+from reflex.utils.exceptions import (
+    InvalidLockWarningThresholdError,
+    InvalidStateManagerModeError,
+    LockExpiredError,
+    StateSchemaMismatchError,
+)
+
+
+@dataclasses.dataclass
+class StateManager(ABC):
+    """A class to manage many client states."""
+
+    # The state class to use.
+    state: type[BaseState]
+
+    @classmethod
+    def create(cls, state: type[BaseState]):
+        """Create a new state manager.
+
+        Args:
+            state: The state class to use.
+
+        Raises:
+            InvalidStateManagerModeError: If the state manager mode is invalid.
+
+        Returns:
+            The state manager (either disk, memory or redis).
+        """
+        config = get_config()
+        if prerequisites.parse_redis_url() is not None:
+            config.state_manager_mode = constants.StateManagerMode.REDIS
+        if config.state_manager_mode == constants.StateManagerMode.MEMORY:
+            return StateManagerMemory(state=state)
+        if config.state_manager_mode == constants.StateManagerMode.DISK:
+            return StateManagerDisk(state=state)
+        if config.state_manager_mode == constants.StateManagerMode.REDIS:
+            redis = prerequisites.get_redis()
+            if redis is not None:
+                # make sure expiration values are obtained only from the config object on creation
+                return StateManagerRedis(
+                    state=state,
+                    redis=redis,
+                    token_expiration=config.redis_token_expiration,
+                    lock_expiration=config.redis_lock_expiration,
+                    lock_warning_threshold=config.redis_lock_warning_threshold,
+                )
+        raise InvalidStateManagerModeError(
+            f"Expected one of: DISK, MEMORY, REDIS, got {config.state_manager_mode}"
+        )
+
+    @abstractmethod
+    async def get_state(self, token: str) -> BaseState:
+        """Get the state for a token.
+
+        Args:
+            token: The token to get the state for.
+
+        Returns:
+            The state for the token.
+        """
+        pass
+
+    @abstractmethod
+    async def set_state(self, token: str, state: BaseState):
+        """Set the state for a token.
+
+        Args:
+            token: The token to set the state for.
+            state: The state to set.
+        """
+        pass
+
+    @abstractmethod
+    @contextlib.asynccontextmanager
+    async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
+        """Modify the state for a token while holding exclusive lock.
+
+        Args:
+            token: The token to modify the state for.
+
+        Yields:
+            The state for the token.
+        """
+        yield self.state()
+
+
+@dataclasses.dataclass
+class StateManagerMemory(StateManager):
+    """A state manager that stores states in memory."""
+
+    # The mapping of client ids to states.
+    states: dict[str, BaseState] = dataclasses.field(default_factory=dict)
+
+    # The mutex ensures the dict of mutexes is updated exclusively
+    _state_manager_lock: asyncio.Lock = dataclasses.field(default=asyncio.Lock())
+
+    # The dict of mutexes for each client
+    _states_locks: dict[str, asyncio.Lock] = dataclasses.field(
+        default_factory=dict, init=False
+    )
+
+    @override
+    async def get_state(self, token: str) -> BaseState:
+        """Get the state for a token.
+
+        Args:
+            token: The token to get the state for.
+
+        Returns:
+            The state for the token.
+        """
+        # Memory state manager ignores the substate suffix and always returns the top-level state.
+        token = _split_substate_key(token)[0]
+        if token not in self.states:
+            self.states[token] = self.state(_reflex_internal_init=True)
+        return self.states[token]
+
+    @override
+    async def set_state(self, token: str, state: BaseState):
+        """Set the state for a token.
+
+        Args:
+            token: The token to set the state for.
+            state: The state to set.
+        """
+        pass
+
+    @override
+    @contextlib.asynccontextmanager
+    async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
+        """Modify the state for a token while holding exclusive lock.
+
+        Args:
+            token: The token to modify the state for.
+
+        Yields:
+            The state for the token.
+        """
+        # Memory state manager ignores the substate suffix and always returns the top-level state.
+        token = _split_substate_key(token)[0]
+        if token not in self._states_locks:
+            async with self._state_manager_lock:
+                if token not in self._states_locks:
+                    self._states_locks[token] = asyncio.Lock()
+
+        async with self._states_locks[token]:
+            state = await self.get_state(token)
+            yield state
+            await self.set_state(token, state)
+
+
+def _default_token_expiration() -> int:
+    """Get the default token expiration time.
+
+    Returns:
+        The default token expiration time.
+    """
+    return get_config().redis_token_expiration
+
+
+def reset_disk_state_manager():
+    """Reset the disk state manager."""
+    states_directory = prerequisites.get_states_dir()
+    if states_directory.exists():
+        for path in states_directory.iterdir():
+            path.unlink()
+
+
+@dataclasses.dataclass
+class StateManagerDisk(StateManager):
+    """A state manager that stores states in memory."""
+
+    # The mapping of client ids to states.
+    states: dict[str, BaseState] = dataclasses.field(default_factory=dict)
+
+    # The mutex ensures the dict of mutexes is updated exclusively
+    _state_manager_lock: asyncio.Lock = dataclasses.field(default=asyncio.Lock())
+
+    # The dict of mutexes for each client
+    _states_locks: dict[str, asyncio.Lock] = dataclasses.field(
+        default_factory=dict,
+        init=False,
+    )
+
+    # The token expiration time (s).
+    token_expiration: int = dataclasses.field(default_factory=_default_token_expiration)
+
+    def __post_init_(self):
+        """Create a new state manager."""
+        path_ops.mkdir(self.states_directory)
+
+        self._purge_expired_states()
+
+    @functools.cached_property
+    def states_directory(self) -> Path:
+        """Get the states directory.
+
+        Returns:
+            The states directory.
+        """
+        return prerequisites.get_states_dir()
+
+    def _purge_expired_states(self):
+        """Purge expired states from the disk."""
+        import time
+
+        for path in path_ops.ls(self.states_directory):
+            # check path is a pickle file
+            if path.suffix != ".pkl":
+                continue
+
+            # load last edited field from file
+            last_edited = path.stat().st_mtime
+
+            # check if the file is older than the token expiration time
+            if time.time() - last_edited > self.token_expiration:
+                # remove the file
+                path.unlink()
+
+    def token_path(self, token: str) -> Path:
+        """Get the path for a token.
+
+        Args:
+            token: The token to get the path for.
+
+        Returns:
+            The path for the token.
+        """
+        return (
+            self.states_directory / f"{md5(token.encode()).hexdigest()}.pkl"
+        ).absolute()
+
+    async def load_state(self, token: str) -> BaseState | None:
+        """Load a state object based on the provided token.
+
+        Args:
+            token: The token used to identify the state object.
+
+        Returns:
+            The loaded state object or None.
+        """
+        token_path = self.token_path(token)
+
+        if token_path.exists():
+            try:
+                with token_path.open(mode="rb") as file:
+                    return BaseState._deserialize(fp=file)
+            except Exception:
+                pass
+
+    async def populate_substates(
+        self, client_token: str, state: BaseState, root_state: BaseState
+    ):
+        """Populate the substates of a state object.
+
+        Args:
+            client_token: The client token.
+            state: The state object to populate.
+            root_state: The root state object.
+        """
+        for substate in state.get_substates():
+            substate_token = _substate_key(client_token, substate)
+
+            fresh_instance = await root_state.get_state(substate)
+            instance = await self.load_state(substate_token)
+            if instance is not None:
+                # Ensure all substates exist, even if they weren't serialized previously.
+                instance.substates = fresh_instance.substates
+            else:
+                instance = fresh_instance
+            state.substates[substate.get_name()] = instance
+            instance.parent_state = state
+
+            await self.populate_substates(client_token, instance, root_state)
+
+    @override
+    async def get_state(
+        self,
+        token: str,
+    ) -> BaseState:
+        """Get the state for a token.
+
+        Args:
+            token: The token to get the state for.
+
+        Returns:
+            The state for the token.
+        """
+        client_token = _split_substate_key(token)[0]
+        root_state = self.states.get(client_token)
+        if root_state is not None:
+            # Retrieved state from memory.
+            return root_state
+
+        # Deserialize root state from disk.
+        root_state = await self.load_state(_substate_key(client_token, self.state))
+        # Create a new root state tree with all substates instantiated.
+        fresh_root_state = self.state(_reflex_internal_init=True)
+        if root_state is None:
+            root_state = fresh_root_state
+        else:
+            # Ensure all substates exist, even if they were not serialized previously.
+            root_state.substates = fresh_root_state.substates
+        self.states[client_token] = root_state
+        await self.populate_substates(client_token, root_state, root_state)
+        return root_state
+
+    async def set_state_for_substate(self, client_token: str, substate: BaseState):
+        """Set the state for a substate.
+
+        Args:
+            client_token: The client token.
+            substate: The substate to set.
+        """
+        substate_token = _substate_key(client_token, substate)
+
+        if substate._get_was_touched():
+            substate._was_touched = False  # Reset the touched flag after serializing.
+            pickle_state = substate._serialize()
+            if pickle_state:
+                if not self.states_directory.exists():
+                    self.states_directory.mkdir(parents=True, exist_ok=True)
+                self.token_path(substate_token).write_bytes(pickle_state)
+
+        for substate_substate in substate.substates.values():
+            await self.set_state_for_substate(client_token, substate_substate)
+
+    @override
+    async def set_state(self, token: str, state: BaseState):
+        """Set the state for a token.
+
+        Args:
+            token: The token to set the state for.
+            state: The state to set.
+        """
+        client_token, substate = _split_substate_key(token)
+        await self.set_state_for_substate(client_token, state)
+
+    @override
+    @contextlib.asynccontextmanager
+    async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
+        """Modify the state for a token while holding exclusive lock.
+
+        Args:
+            token: The token to modify the state for.
+
+        Yields:
+            The state for the token.
+        """
+        # Memory state manager ignores the substate suffix and always returns the top-level state.
+        client_token, substate = _split_substate_key(token)
+        if client_token not in self._states_locks:
+            async with self._state_manager_lock:
+                if client_token not in self._states_locks:
+                    self._states_locks[client_token] = asyncio.Lock()
+
+        async with self._states_locks[client_token]:
+            state = await self.get_state(token)
+            yield state
+            await self.set_state(token, state)
+
+
+def _default_lock_expiration() -> int:
+    """Get the default lock expiration time.
+
+    Returns:
+        The default lock expiration time.
+    """
+    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
+
+
+@dataclasses.dataclass
+class StateManagerRedis(StateManager):
+    """A state manager that stores states in redis."""
+
+    # The redis client to use.
+    redis: Redis
+
+    # The token expiration time (s).
+    token_expiration: int = dataclasses.field(default_factory=_default_token_expiration)
+
+    # The maximum time to hold a lock (ms).
+    lock_expiration: int = dataclasses.field(default_factory=_default_lock_expiration)
+
+    # The maximum time to hold a lock (ms) before warning.
+    lock_warning_threshold: int = dataclasses.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 = dataclasses.field(
+        default="K"  # Enable keyspace notifications (target a particular key)
+        "g"  # For generic commands (DEL, EXPIRE, etc)
+        "x"  # For expired events
+        "e"  # For evicted events (i.e. maxmemory exceeded)
+    )
+
+    # These events indicate that a lock is no longer held.
+    _redis_keyspace_lock_release_events: set[bytes] = dataclasses.field(
+        default_factory=lambda: {
+            b"del",
+            b"expire",
+            b"expired",
+            b"evicted",
+        }
+    )
+
+    # Whether keyspace notifications have been enabled.
+    _redis_notify_keyspace_events_enabled: bool = dataclasses.field(default=False)
+
+    # The logical database number used by the redis client.
+    _redis_db: int = dataclasses.field(default=0)
+
+    def __post_init__(self):
+        """Validate the lock warning threshold.
+
+        Raises:
+            InvalidLockWarningThresholdError: If the lock warning threshold is invalid.
+        """
+        if self.lock_warning_threshold >= (lock_expiration := self.lock_expiration):
+            raise InvalidLockWarningThresholdError(
+                f"The lock warning threshold({self.lock_warning_threshold}) must be less than the lock expiration time({lock_expiration})."
+            )
+
+    def _get_required_state_classes(
+        self,
+        target_state_cls: type[BaseState],
+        subclasses: bool = False,
+        required_state_classes: set[type[BaseState]] | None = None,
+    ) -> set[type[BaseState]]:
+        """Recursively determine which states are required to fetch the target state.
+
+        This will always include potentially dirty substates that depend on vars
+        in the target_state_cls.
+
+        Args:
+            target_state_cls: The target state class being fetched.
+            subclasses: Whether to include subclasses of the target state.
+            required_state_classes: Recursive argument tracking state classes that have already been seen.
+
+        Returns:
+            The set of state classes required to fetch the target state.
+        """
+        if required_state_classes is None:
+            required_state_classes = set()
+        # Get the substates if requested.
+        if subclasses:
+            for substate in target_state_cls.get_substates():
+                self._get_required_state_classes(
+                    substate,
+                    subclasses=True,
+                    required_state_classes=required_state_classes,
+                )
+        if target_state_cls in required_state_classes:
+            return required_state_classes
+        required_state_classes.add(target_state_cls)
+
+        # Get dependent substates.
+        for pd_substates in target_state_cls._get_potentially_dirty_states():
+            self._get_required_state_classes(
+                pd_substates,
+                subclasses=False,
+                required_state_classes=required_state_classes,
+            )
+
+        # Get the parent state if it exists.
+        if parent_state := target_state_cls.get_parent_state():
+            self._get_required_state_classes(
+                parent_state,
+                subclasses=False,
+                required_state_classes=required_state_classes,
+            )
+        return required_state_classes
+
+    def _get_populated_states(
+        self,
+        target_state: BaseState,
+        populated_states: dict[str, BaseState] | None = None,
+    ) -> dict[str, BaseState]:
+        """Recursively determine which states from target_state are already fetched.
+
+        Args:
+            target_state: The state to check for populated states.
+            populated_states: Recursive argument tracking states seen in previous calls.
+
+        Returns:
+            A dictionary of state full name to state instance.
+        """
+        if populated_states is None:
+            populated_states = {}
+        if target_state.get_full_name() in populated_states:
+            return populated_states
+        populated_states[target_state.get_full_name()] = target_state
+        for substate in target_state.substates.values():
+            self._get_populated_states(substate, populated_states=populated_states)
+        if target_state.parent_state is not None:
+            self._get_populated_states(
+                target_state.parent_state, populated_states=populated_states
+            )
+        return populated_states
+
+    @override
+    async def get_state(
+        self,
+        token: str,
+        top_level: bool = True,
+        for_state_instance: BaseState | None = None,
+    ) -> BaseState:
+        """Get the state for a token.
+
+        Args:
+            token: The token to get the state for.
+            top_level: If true, return an instance of the top-level state (self.state).
+            for_state_instance: If provided, attach the requested states to this existing state tree.
+
+        Returns:
+            The state for the token.
+
+        Raises:
+            RuntimeError: when the state_cls is not specified in the token, or when the parent state for a
+                requested state was not fetched.
+        """
+        # Split the actual token from the fully qualified substate name.
+        token, state_path = _split_substate_key(token)
+        if state_path:
+            # Get the State class associated with the given path.
+            state_cls = self.state.get_class_substate(state_path)
+        else:
+            raise RuntimeError(
+                f"StateManagerRedis requires token to be specified in the form of {{token}}_{{state_full_name}}, but got {token}"
+            )
+
+        # Determine which states we already have.
+        flat_state_tree: dict[str, BaseState] = (
+            self._get_populated_states(for_state_instance) if for_state_instance else {}
+        )
+
+        # Determine which states from the tree need to be fetched.
+        required_state_classes = sorted(
+            self._get_required_state_classes(state_cls, subclasses=True)
+            - {type(s) for s in flat_state_tree.values()},
+            key=lambda x: x.get_full_name(),
+        )
+
+        redis_pipeline = self.redis.pipeline()
+        for state_cls in required_state_classes:
+            redis_pipeline.get(_substate_key(token, state_cls))
+
+        for state_cls, redis_state in zip(
+            required_state_classes,
+            await redis_pipeline.execute(),
+            strict=False,
+        ):
+            state = None
+
+            if redis_state is not None:
+                # Deserialize the substate.
+                with contextlib.suppress(StateSchemaMismatchError):
+                    state = BaseState._deserialize(data=redis_state)
+            if state is None:
+                # Key didn't exist or schema mismatch so create a new instance for this token.
+                state = state_cls(
+                    init_substates=False,
+                    _reflex_internal_init=True,
+                )
+            flat_state_tree[state.get_full_name()] = state
+            if state.get_parent_state() is not None:
+                parent_state_name, _dot, state_name = state.get_full_name().rpartition(
+                    "."
+                )
+                parent_state = flat_state_tree.get(parent_state_name)
+                if parent_state is None:
+                    raise RuntimeError(
+                        f"Parent state for {state.get_full_name()} was not found "
+                        "in the state tree, but should have already been fetched. "
+                        "This is a bug",
+                    )
+                parent_state.substates[state_name] = state
+                state.parent_state = parent_state
+
+        # To retain compatibility with previous implementation, by default, we return
+        # the top-level state which should always be fetched or already cached.
+        if top_level:
+            return flat_state_tree[self.state.get_full_name()]
+        return flat_state_tree[state_cls.get_full_name()]
+
+    @override
+    async def set_state(
+        self,
+        token: str,
+        state: BaseState,
+        lock_id: bytes | None = None,
+    ):
+        """Set the state for a token.
+
+        Args:
+            token: The token to set the state for.
+            state: The state to set.
+            lock_id: If provided, the lock_key must be set to this value to set the state.
+
+        Raises:
+            LockExpiredError: If lock_id is provided and the lock for the token is not held by that ID.
+            RuntimeError: If the state instance doesn't match the state name in the token.
+        """
+        # Check that we're holding the lock.
+        if (
+            lock_id is not None
+            and await self.redis.get(self._lock_key(token)) != lock_id
+        ):
+            raise LockExpiredError(
+                f"Lock expired for token {token} while processing. Consider increasing "
+                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:
+            raise RuntimeError(
+                f"Cannot `set_state` with mismatching token {token} and substate {state.get_full_name()}."
+            )
+
+        # Recursively set_state on all known substates.
+        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()
+            if pickle_state:
+                await self.redis.set(
+                    _substate_key(client_token, state),
+                    pickle_state,
+                    ex=self.token_expiration,
+                )
+
+        # Wait for substates to be persisted.
+        for t in tasks:
+            await t
+
+    @override
+    @contextlib.asynccontextmanager
+    async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
+        """Modify the state for a token while holding exclusive lock.
+
+        Args:
+            token: The token to modify the state for.
+
+        Yields:
+            The state for the token.
+        """
+        async with self._lock(token) as lock_id:
+            state = await self.get_state(token)
+            yield state
+            await self.set_state(token, state, lock_id)
+
+    @staticmethod
+    def _lock_key(token: str) -> bytes:
+        """Get the redis key for a token's lock.
+
+        Args:
+            token: The token to get the lock key for.
+
+        Returns:
+            The redis lock key for the token.
+        """
+        # All substates share the same lock domain, so ignore any substate path suffix.
+        client_token = _split_substate_key(token)[0]
+        return f"{client_token}_lock".encode()
+
+    async def _try_get_lock(self, lock_key: bytes, lock_id: bytes) -> bool | None:
+        """Try to get a redis lock for a token.
+
+        Args:
+            lock_key: The redis key for the lock.
+            lock_id: The ID of the lock.
+
+        Returns:
+            True if the lock was obtained.
+        """
+        return await self.redis.set(
+            lock_key,
+            lock_id,
+            px=self.lock_expiration,
+            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 _enable_keyspace_notifications(self):
+        """Enable keyspace notifications for the redis server.
+
+        Raises:
+            ResponseError: when the keyspace config cannot be set.
+        """
+        if self._redis_notify_keyspace_events_enabled:
+            return
+        # Find out which logical database index is being used.
+        self._redis_db = self.redis.get_connection_kwargs().get("db", self._redis_db)
+
+        try:
+            await self.redis.config_set(
+                "notify-keyspace-events",
+                self._redis_notify_keyspace_events,
+            )
+        except ResponseError:
+            # Some redis servers only allow out-of-band configuration, so ignore errors here.
+            if not environment.REFLEX_IGNORE_REDIS_CONFIG_ERROR.get():
+                raise
+        self._redis_notify_keyspace_events_enabled = True
+
+    async def _wait_lock(self, lock_key: bytes, lock_id: bytes) -> None:
+        """Wait for a redis lock to be released via pubsub.
+
+        Coroutine will not return until the lock is obtained.
+
+        Args:
+            lock_key: The redis key for the lock.
+            lock_id: The ID of the lock.
+        """
+        # Enable keyspace notifications for the lock key, so we know when it is available.
+        await self._enable_keyspace_notifications()
+        lock_key_channel = f"__keyspace@{self._redis_db}__:{lock_key.decode()}"
+        async with self.redis.pubsub() as pubsub:
+            await pubsub.psubscribe(lock_key_channel)
+            # 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):
+        """Obtain a redis lock for a token.
+
+        Args:
+            token: The token to obtain a lock for.
+
+        Yields:
+            The ID of the lock (to be passed to set_state).
+
+        Raises:
+            LockExpiredError: If the lock has expired while processing the event.
+        """
+        lock_key = self._lock_key(token)
+        lock_id = uuid.uuid4().hex.encode()
+
+        if not await self._try_get_lock(lock_key, lock_id):
+            # Missed the fast-path to get lock, subscribe for lock delete/expire events
+            await self._wait_lock(lock_key, lock_id)
+        state_is_locked = True
+
+        try:
+            yield lock_id
+        except LockExpiredError:
+            state_is_locked = False
+            raise
+        finally:
+            if state_is_locked:
+                # only delete our lock
+                await self.redis.delete(lock_key)
+
+    async def close(self):
+        """Explicitly close the redis connection and connection_pool.
+
+        It is necessary in testing scenarios to close between asyncio test cases
+        to avoid having lingering redis connections associated with event loops
+        that will be closed (each test case uses its own event loop).
+
+        Note: Connections will be automatically reopened when needed.
+        """
+        await self.redis.aclose(close_connection_pool=True)
+
+
+def get_state_manager() -> StateManager:
+    """Get the state manager for the app that is currently running.
+
+    Returns:
+        The state manager.
+    """
+    return prerequisites.get_and_validate_app().app.state_manager

+ 726 - 2
reflex/istate/proxy.py

@@ -1,8 +1,309 @@
 """A module to hold state proxy classes."""
 
-from typing import Any
+from __future__ import annotations
 
-from reflex.state import StateProxy
+import asyncio
+import copy
+import dataclasses
+import functools
+import inspect
+import json
+from collections.abc import Callable, Sequence
+from types import MethodType
+from typing import TYPE_CHECKING, Any, SupportsIndex
+
+import pydantic
+import wrapt
+from pydantic import BaseModel as BaseModelV2
+from pydantic.v1 import BaseModel as BaseModelV1
+from sqlalchemy.orm import DeclarativeBase
+
+from reflex.base import Base
+from reflex.utils import prerequisites
+from reflex.utils.exceptions import ImmutableStateError
+from reflex.utils.serializers import serializer
+from reflex.vars.base import Var
+
+if TYPE_CHECKING:
+    from reflex.state import BaseState, StateUpdate
+
+
+class StateProxy(wrapt.ObjectProxy):
+    """Proxy of a state instance to control mutability of vars for a background task.
+
+    Since a background task runs against a state instance without holding the
+    state_manager lock for the token, the reference may become stale if the same
+    state is modified by another event handler.
+
+    The proxy object ensures that writes to the state are blocked unless
+    explicitly entering a context which refreshes the state from state_manager
+    and holds the lock for the token until exiting the context. After exiting
+    the context, a StateUpdate may be emitted to the frontend to notify the
+    client of the state change.
+
+    A background task will be passed the `StateProxy` as `self`, so mutability
+    can be safely performed inside an `async with self` block.
+
+        class State(rx.State):
+            counter: int = 0
+
+            @rx.event(background=True)
+            async def bg_increment(self):
+                await asyncio.sleep(1)
+                async with self:
+                    self.counter += 1
+    """
+
+    def __init__(
+        self,
+        state_instance: BaseState,
+        parent_state_proxy: StateProxy | None = None,
+    ):
+        """Create a proxy for a state instance.
+
+        If `get_state` is used on a StateProxy, the resulting state will be
+        linked to the given state via parent_state_proxy. The first state in the
+        chain is the state that initiated the background task.
+
+        Args:
+            state_instance: The state instance to proxy.
+            parent_state_proxy: The parent state proxy, for linked mutability and context tracking.
+        """
+        super().__init__(state_instance)
+        # compile is not relevant to backend logic
+        self._self_app = prerequisites.get_and_validate_app().app
+        self._self_substate_path = tuple(state_instance.get_full_name().split("."))
+        self._self_actx = None
+        self._self_mutable = False
+        self._self_actx_lock = asyncio.Lock()
+        self._self_actx_lock_holder = None
+        self._self_parent_state_proxy = parent_state_proxy
+
+    def _is_mutable(self) -> bool:
+        """Check if the state is mutable.
+
+        Returns:
+            Whether the state is mutable.
+        """
+        if self._self_parent_state_proxy is not None:
+            return self._self_parent_state_proxy._is_mutable() or self._self_mutable
+        return self._self_mutable
+
+    async def __aenter__(self) -> StateProxy:
+        """Enter the async context manager protocol.
+
+        Sets mutability to True and enters the `App.modify_state` async context,
+        which refreshes the state from state_manager and holds the lock for the
+        given state token until exiting the context.
+
+        Background tasks should avoid blocking calls while inside the context.
+
+        Returns:
+            This StateProxy instance in mutable mode.
+
+        Raises:
+            ImmutableStateError: If the state is already mutable.
+        """
+        if self._self_parent_state_proxy is not None:
+            from reflex.state import State
+
+            parent_state = (
+                await self._self_parent_state_proxy.__aenter__()
+            ).__wrapped__
+            super().__setattr__(
+                "__wrapped__",
+                await parent_state.get_state(
+                    State.get_class_substate(self._self_substate_path)
+                ),
+            )
+            return self
+        current_task = asyncio.current_task()
+        if (
+            self._self_actx_lock.locked()
+            and current_task == self._self_actx_lock_holder
+        ):
+            raise ImmutableStateError(
+                "The state is already mutable. Do not nest `async with self` blocks."
+            )
+
+        from reflex.state import _substate_key
+
+        await self._self_actx_lock.acquire()
+        self._self_actx_lock_holder = current_task
+        self._self_actx = self._self_app.modify_state(
+            token=_substate_key(
+                self.__wrapped__.router.session.client_token,
+                self._self_substate_path,
+            )
+        )
+        mutable_state = await self._self_actx.__aenter__()
+        super().__setattr__(
+            "__wrapped__", mutable_state.get_substate(self._self_substate_path)
+        )
+        self._self_mutable = True
+        return self
+
+    async def __aexit__(self, *exc_info: Any) -> None:
+        """Exit the async context manager protocol.
+
+        Sets proxy mutability to False and persists any state changes.
+
+        Args:
+            exc_info: The exception info tuple.
+        """
+        if self._self_parent_state_proxy is not None:
+            await self._self_parent_state_proxy.__aexit__(*exc_info)
+            return
+        if self._self_actx is None:
+            return
+        self._self_mutable = False
+        try:
+            await self._self_actx.__aexit__(*exc_info)
+        finally:
+            self._self_actx_lock_holder = None
+            self._self_actx_lock.release()
+        self._self_actx = None
+
+    def __enter__(self):
+        """Enter the regular context manager protocol.
+
+        This is not supported for background tasks, and exists only to raise a more useful exception
+        when the StateProxy is used incorrectly.
+
+        Raises:
+            TypeError: always, because only async contextmanager protocol is supported.
+        """
+        raise TypeError("Background task must use `async with self` to modify state.")
+
+    def __exit__(self, *exc_info: Any) -> None:
+        """Exit the regular context manager protocol.
+
+        Args:
+            exc_info: The exception info tuple.
+        """
+        pass
+
+    def __getattr__(self, name: str) -> Any:
+        """Get the attribute from the underlying state instance.
+
+        Args:
+            name: The name of the attribute.
+
+        Returns:
+            The value of the attribute.
+
+        Raises:
+            ImmutableStateError: If the state is not in mutable mode.
+        """
+        if name in ["substates", "parent_state"] and not self._is_mutable():
+            raise ImmutableStateError(
+                "Background task StateProxy is immutable outside of a context "
+                "manager. Use `async with self` to modify state."
+            )
+
+        value = super().__getattr__(name)
+        if not name.startswith("_self_") and isinstance(value, MutableProxy):
+            # ensure mutations to these containers are blocked unless proxy is _mutable
+            return ImmutableMutableProxy(
+                wrapped=value.__wrapped__,
+                state=self,
+                field_name=value._self_field_name,
+            )
+        if isinstance(value, functools.partial) and value.args[0] is self.__wrapped__:
+            # Rebind event handler to the proxy instance
+            value = functools.partial(
+                value.func,
+                self,
+                *value.args[1:],
+                **value.keywords,
+            )
+        if isinstance(value, MethodType) and value.__self__ is self.__wrapped__:
+            # Rebind methods to the proxy instance
+            value = type(value)(value.__func__, self)
+        return value
+
+    def __setattr__(self, name: str, value: Any) -> None:
+        """Set the attribute on the underlying state instance.
+
+        If the attribute is internal, set it on the proxy instance instead.
+
+        Args:
+            name: The name of the attribute.
+            value: The value of the attribute.
+
+        Raises:
+            ImmutableStateError: If the state is not in mutable mode.
+        """
+        if (
+            name.startswith("_self_")  # wrapper attribute
+            or self._is_mutable()  # lock held
+            # non-persisted state attribute
+            or name in self.__wrapped__.get_skip_vars()
+        ):
+            super().__setattr__(name, value)
+            return
+
+        raise ImmutableStateError(
+            "Background task StateProxy is immutable outside of a context "
+            "manager. Use `async with self` to modify state."
+        )
+
+    def get_substate(self, path: Sequence[str]) -> BaseState:
+        """Only allow substate access with lock held.
+
+        Args:
+            path: The path to the substate.
+
+        Returns:
+            The substate.
+
+        Raises:
+            ImmutableStateError: If the state is not in mutable mode.
+        """
+        if not self._is_mutable():
+            raise ImmutableStateError(
+                "Background task StateProxy is immutable outside of a context "
+                "manager. Use `async with self` to modify state."
+            )
+        return self.__wrapped__.get_substate(path)
+
+    async def get_state(self, state_cls: type[BaseState]) -> BaseState:
+        """Get an instance of the state associated with this token.
+
+        Args:
+            state_cls: The class of the state.
+
+        Returns:
+            The state.
+
+        Raises:
+            ImmutableStateError: If the state is not in mutable mode.
+        """
+        if not self._is_mutable():
+            raise ImmutableStateError(
+                "Background task StateProxy is immutable outside of a context "
+                "manager. Use `async with self` to modify state."
+            )
+        return type(self)(
+            await self.__wrapped__.get_state(state_cls), parent_state_proxy=self
+        )
+
+    async def _as_state_update(self, *args, **kwargs) -> StateUpdate:
+        """Temporarily allow mutability to access parent_state.
+
+        Args:
+            *args: The args to pass to the underlying state instance.
+            **kwargs: The kwargs to pass to the underlying state instance.
+
+        Returns:
+            The state update.
+        """
+        original_mutable = self._self_mutable
+        self._self_mutable = True
+        try:
+            return await self.__wrapped__._as_state_update(*args, **kwargs)
+        finally:
+            self._self_mutable = original_mutable
 
 
 class ReadOnlyStateProxy(StateProxy):
@@ -31,3 +332,426 @@ class ReadOnlyStateProxy(StateProxy):
             NotImplementedError: Always raised when trying to mark the proxied state as dirty.
         """
         raise NotImplementedError("This is a read-only state proxy.")
+
+
+class MutableProxy(wrapt.ObjectProxy):
+    """A proxy for a mutable object that tracks changes."""
+
+    # Hint for finding the base class of the proxy.
+    __base_proxy__ = "MutableProxy"
+
+    # Methods on wrapped objects which should mark the state as dirty.
+    __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__ = {
+        "get",
+        "setdefault",
+    }
+
+    # These internal attributes on rx.Base should NOT be wrapped in a MutableProxy.
+    __never_wrap_base_attrs__ = set(Base.__dict__) - {"set"} | set(
+        pydantic.BaseModel.__dict__
+    )
+
+    # These types will be wrapped in MutableProxy
+    __mutable_types__ = (
+        list,
+        dict,
+        set,
+        Base,
+        DeclarativeBase,
+        BaseModelV2,
+        BaseModelV1,
+    )
+
+    # Dynamically generated classes for tracking dataclass mutations.
+    __dataclass_proxies__: dict[type, type] = {}
+
+    def __new__(cls, wrapped: Any, *args, **kwargs) -> MutableProxy:
+        """Create a proxy instance for a mutable object that tracks changes.
+
+        Args:
+            wrapped: The object to proxy.
+            *args: Other args passed to MutableProxy (ignored).
+            **kwargs: Other kwargs passed to MutableProxy (ignored).
+
+        Returns:
+            The proxy instance.
+        """
+        if dataclasses.is_dataclass(wrapped):
+            wrapped_cls = type(wrapped)
+            wrapper_cls_name = wrapped_cls.__name__ + cls.__name__
+            # Find the associated class
+            if wrapper_cls_name not in cls.__dataclass_proxies__:
+                # Create a new class that has the __dataclass_fields__ defined
+                cls.__dataclass_proxies__[wrapper_cls_name] = type(
+                    wrapper_cls_name,
+                    (cls,),
+                    {
+                        dataclasses._FIELDS: getattr(  # pyright: ignore [reportAttributeAccessIssue]
+                            wrapped_cls,
+                            dataclasses._FIELDS,  # pyright: ignore [reportAttributeAccessIssue]
+                        ),
+                    },
+                )
+            cls = cls.__dataclass_proxies__[wrapper_cls_name]
+        return super().__new__(cls)
+
+    def __init__(self, wrapped: Any, state: BaseState, field_name: str):
+        """Create a proxy for a mutable object that tracks changes.
+
+        Args:
+            wrapped: The object to proxy.
+            state: The state to mark dirty when the object is changed.
+            field_name: The name of the field on the state associated with the
+                wrapped object.
+        """
+        super().__init__(wrapped)
+        self._self_state = state
+        self._self_field_name = field_name
+
+    def __repr__(self) -> str:
+        """Get the representation of the wrapped object.
+
+        Returns:
+            The representation of the wrapped object.
+        """
+        return f"{type(self).__name__}({self.__wrapped__})"
+
+    def _mark_dirty(
+        self,
+        wrapped: Callable | None = None,
+        instance: BaseState | None = None,
+        args: tuple = (),
+        kwargs: dict | None = None,
+    ) -> Any:
+        """Mark the state as dirty, then call a wrapped function.
+
+        Intended for use with `FunctionWrapper` from the `wrapt` library.
+
+        Args:
+            wrapped: The wrapped function.
+            instance: The instance of the wrapped function.
+            args: The args for the wrapped function.
+            kwargs: The kwargs for the wrapped function.
+
+        Returns:
+            The result of the wrapped function.
+        """
+        self._self_state.dirty_vars.add(self._self_field_name)
+        self._self_state._mark_dirty()
+        if wrapped is not None:
+            return wrapped(*args, **(kwargs or {}))
+
+    @classmethod
+    def _is_mutable_type(cls, value: Any) -> bool:
+        """Check if a value is of a mutable type and should be wrapped.
+
+        Args:
+            value: The value to check.
+
+        Returns:
+            Whether the value is of a mutable type.
+        """
+        return isinstance(value, cls.__mutable_types__) or (
+            dataclasses.is_dataclass(value) and not isinstance(value, Var)
+        )
+
+    @staticmethod
+    def _is_called_from_dataclasses_internal() -> bool:
+        """Check if the current function is called from dataclasses helper.
+
+        Returns:
+            Whether the current function is called from dataclasses internal code.
+        """
+        # Walk up the stack a bit to see if we are called from dataclasses
+        # internal code, for example `asdict` or `astuple`.
+        frame = inspect.currentframe()
+        for _ in range(5):
+            # Why not `inspect.stack()` -- this is much faster!
+            if not (frame := frame and frame.f_back):
+                break
+            if inspect.getfile(frame) == dataclasses.__file__:
+                return True
+        return False
+
+    def _wrap_recursive(self, value: Any) -> Any:
+        """Wrap a value recursively if it is mutable.
+
+        Args:
+            value: The value to wrap.
+
+        Returns:
+            The wrapped value.
+        """
+        # When called from dataclasses internal code, return the unwrapped value
+        if self._is_called_from_dataclasses_internal():
+            return value
+        # Recursively wrap mutable types, but do not re-wrap MutableProxy instances.
+        if self._is_mutable_type(value) and not isinstance(value, MutableProxy):
+            base_cls = globals()[self.__base_proxy__]
+            return base_cls(
+                wrapped=value,
+                state=self._self_state,
+                field_name=self._self_field_name,
+            )
+        return value
+
+    def _wrap_recursive_decorator(
+        self, wrapped: Callable, instance: BaseState, args: list, kwargs: dict
+    ) -> Any:
+        """Wrap a function that returns a possibly mutable value.
+
+        Intended for use with `FunctionWrapper` from the `wrapt` library.
+
+        Args:
+            wrapped: The wrapped function.
+            instance: The instance of the wrapped function.
+            args: The args for the wrapped function.
+            kwargs: The kwargs for the wrapped function.
+
+        Returns:
+            The result of the wrapped function (possibly wrapped in a MutableProxy).
+        """
+        return self._wrap_recursive(wrapped(*args, **kwargs))
+
+    def __getattr__(self, __name: str) -> Any:
+        """Get the attribute on the proxied object and return a proxy if mutable.
+
+        Args:
+            __name: The name of the attribute.
+
+        Returns:
+            The attribute value.
+        """
+        value = super().__getattr__(__name)
+
+        if callable(value):
+            if __name in self.__mark_dirty_attrs__:
+                # Wrap special callables, like "append", which should mark state dirty.
+                value = wrapt.FunctionWrapper(value, self._mark_dirty)
+
+            if __name in self.__wrap_mutable_attrs__:
+                # Wrap methods that may return mutable objects tied to the state.
+                value = wrapt.FunctionWrapper(
+                    value,
+                    self._wrap_recursive_decorator,
+                )
+
+            if (
+                isinstance(self.__wrapped__, Base)
+                and __name not in self.__never_wrap_base_attrs__
+                and hasattr(value, "__func__")
+            ):
+                # Wrap methods called on Base subclasses, which might do _anything_
+                return wrapt.FunctionWrapper(
+                    functools.partial(value.__func__, self),  # pyright: ignore [reportFunctionMemberAccess]
+                    self._wrap_recursive_decorator,
+                )
+
+        if self._is_mutable_type(value) and __name not in (
+            "__wrapped__",
+            "_self_state",
+            "__dict__",
+        ):
+            # Recursively wrap mutable attribute values retrieved through this proxy.
+            return self._wrap_recursive(value)
+
+        return value
+
+    def __getitem__(self, key: Any) -> Any:
+        """Get the item on the proxied object and return a proxy if mutable.
+
+        Args:
+            key: The key of the item.
+
+        Returns:
+            The item value.
+        """
+        value = super().__getitem__(key)
+        if isinstance(key, slice) and isinstance(value, list):
+            return [self._wrap_recursive(item) for item in value]
+        # Recursively wrap mutable items retrieved through this proxy.
+        return self._wrap_recursive(value)
+
+    def __iter__(self) -> Any:
+        """Iterate over the proxied object and return a proxy if mutable.
+
+        Yields:
+            Each item value (possibly wrapped in MutableProxy).
+        """
+        for value in super().__iter__():
+            # Recursively wrap mutable items retrieved through this proxy.
+            yield self._wrap_recursive(value)
+
+    def __delattr__(self, name: str):
+        """Delete the attribute on the proxied object and mark state dirty.
+
+        Args:
+            name: The name of the attribute.
+        """
+        self._mark_dirty(super().__delattr__, args=(name,))
+
+    def __delitem__(self, key: str):
+        """Delete the item on the proxied object and mark state dirty.
+
+        Args:
+            key: The key of the item.
+        """
+        self._mark_dirty(super().__delitem__, args=(key,))
+
+    def __setitem__(self, key: str, value: Any):
+        """Set the item on the proxied object and mark state dirty.
+
+        Args:
+            key: The key of the item.
+            value: The value of the item.
+        """
+        self._mark_dirty(super().__setitem__, args=(key, value))
+
+    def __setattr__(self, name: str, value: Any):
+        """Set the attribute on the proxied object and mark state dirty.
+
+        If the attribute starts with "_self_", then the state is NOT marked
+        dirty as these are internal proxy attributes.
+
+        Args:
+            name: The name of the attribute.
+            value: The value of the attribute.
+        """
+        if name.startswith("_self_"):
+            # Special case attributes of the proxy itself, not applied to the wrapped object.
+            super().__setattr__(name, value)
+            return
+        self._mark_dirty(super().__setattr__, args=(name, value))
+
+    def __copy__(self) -> Any:
+        """Return a copy of the proxy.
+
+        Returns:
+            A copy of the wrapped object, unconnected to the proxy.
+        """
+        return copy.copy(self.__wrapped__)
+
+    def __deepcopy__(self, memo: dict[int, Any] | None = None) -> Any:
+        """Return a deepcopy of the proxy.
+
+        Args:
+            memo: The memo dict to use for the deepcopy.
+
+        Returns:
+            A deepcopy of the wrapped object, unconnected to the proxy.
+        """
+        return copy.deepcopy(self.__wrapped__, memo=memo)
+
+    def __reduce_ex__(self, protocol_version: SupportsIndex):
+        """Get the state for redis serialization.
+
+        This method is called by cloudpickle to serialize the object.
+
+        It explicitly serializes the wrapped object, stripping off the mutable proxy.
+
+        Args:
+            protocol_version: The protocol version.
+
+        Returns:
+            Tuple of (wrapped class, empty args, class __getstate__)
+        """
+        return self.__wrapped__.__reduce_ex__(protocol_version)
+
+
+@serializer
+def serialize_mutable_proxy(mp: MutableProxy):
+    """Return the wrapped value of a MutableProxy.
+
+    Args:
+        mp: The MutableProxy to serialize.
+
+    Returns:
+        The wrapped object.
+    """
+    return mp.__wrapped__
+
+
+_orig_json_encoder_default = json.JSONEncoder.default
+
+
+def _json_encoder_default_wrapper(self: json.JSONEncoder, o: Any) -> Any:
+    """Wrap JSONEncoder.default to handle MutableProxy objects.
+
+    Args:
+        self: the JSONEncoder instance.
+        o: the object to serialize.
+
+    Returns:
+        A JSON-able object.
+    """
+    try:
+        return o.__wrapped__
+    except AttributeError:
+        pass
+    return _orig_json_encoder_default(self, o)
+
+
+json.JSONEncoder.default = _json_encoder_default_wrapper
+
+
+class ImmutableMutableProxy(MutableProxy):
+    """A proxy for a mutable object that tracks changes.
+
+    This wrapper comes from StateProxy, and will raise an exception if an attempt is made
+    to modify the wrapped object when the StateProxy is immutable.
+    """
+
+    # Ensure that recursively wrapped proxies use ImmutableMutableProxy as base.
+    __base_proxy__ = "ImmutableMutableProxy"
+
+    def _mark_dirty(
+        self,
+        wrapped: Callable | None = None,
+        instance: BaseState | None = None,
+        args: tuple = (),
+        kwargs: dict | None = None,
+    ) -> Any:
+        """Raise an exception when an attempt is made to modify the object.
+
+        Intended for use with `FunctionWrapper` from the `wrapt` library.
+
+        Args:
+            wrapped: The wrapped function.
+            instance: The instance of the wrapped function.
+            args: The args for the wrapped function.
+            kwargs: The kwargs for the wrapped function.
+
+        Returns:
+            The result of the wrapped function.
+
+        Raises:
+            ImmutableStateError: if the StateProxy is not mutable.
+        """
+        if not self._self_state._is_mutable():
+            raise ImmutableStateError(
+                "Background task StateProxy is immutable outside of a context "
+                "manager. Use `async with self` to modify state."
+            )
+        return super()._mark_dirty(
+            wrapped=wrapped, instance=instance, args=args, kwargs=kwargs
+        )

+ 2 - 2
reflex/istate/storage.py

@@ -90,7 +90,7 @@ class LocalStorage(ClientStorageBase, str):
         /,
         name: str | None = None,
         sync: bool = False,
-    ) -> "LocalStorage":
+    ) -> LocalStorage:
         """Create a client-side localStorage (str).
 
         Args:
@@ -124,7 +124,7 @@ class SessionStorage(ClientStorageBase, str):
         errors: str | None = None,
         /,
         name: str | None = None,
-    ) -> "SessionStorage":
+    ) -> SessionStorage:
         """Create a client-side sessionStorage (str).
 
         Args:

+ 3 - 3
reflex/model.py

@@ -5,7 +5,7 @@ from __future__ import annotations
 import re
 from collections import defaultdict
 from contextlib import suppress
-from typing import Any, ClassVar, Type
+from typing import Any, ClassVar
 
 import alembic.autogenerate
 import alembic.command
@@ -161,7 +161,7 @@ async def get_db_status() -> dict[str, bool]:
     return {"db": status}
 
 
-SQLModelOrSqlAlchemy = Type[sqlmodel.SQLModel] | Type[sqlalchemy.orm.DeclarativeBase]
+SQLModelOrSqlAlchemy = type[sqlmodel.SQLModel] | type[sqlalchemy.orm.DeclarativeBase]
 
 
 class ModelRegistry:
@@ -328,7 +328,7 @@ class Model(Base, sqlmodel.SQLModel):  # pyright: ignore [reportGeneralTypeIssue
     def _alembic_render_item(
         type_: str,
         obj: Any,
-        autogen_context: "alembic.autogenerate.api.AutogenContext",
+        autogen_context: alembic.autogenerate.api.AutogenContext,
     ):
         """Alembic render_item hook call.
 

+ 11 - 2
reflex/page.py

@@ -3,12 +3,14 @@
 from __future__ import annotations
 
 from collections import defaultdict
-from typing import Any, Callable, List
+from collections.abc import Callable
+from typing import Any
 
 from reflex.config import get_config
 from reflex.event import EventType
+from reflex.utils import console
 
-DECORATED_PAGES: dict[str, List] = defaultdict(list)
+DECORATED_PAGES: dict[str, list] = defaultdict(list)
 
 
 def page(
@@ -75,6 +77,13 @@ def get_decorated_pages(omit_implicit_routes: bool = True) -> list[dict[str, Any
     Returns:
         The decorated pages.
     """
+    console.deprecate(
+        "get_decorated_pages",
+        reason="This function is deprecated and will be removed in a future version.",
+        deprecation_version="0.7.9",
+        removal_version="0.8.0",
+        dedupe=True,
+    )
     return sorted(
         [
             page_data

+ 287 - 269
reflex/reflex.py

@@ -3,74 +3,63 @@
 from __future__ import annotations
 
 import atexit
+from importlib.util import find_spec
 from pathlib import Path
 from typing import TYPE_CHECKING
 
-import typer
-import typer.core
+import click
 from reflex_cli.v2.deployments import hosting_cli
 
 from reflex import constants
 from reflex.config import environment, get_config
+from reflex.constants.base import LITERAL_ENV
 from reflex.custom_components.custom_components import custom_components_cli
 from reflex.state import reset_disk_state_manager
 from reflex.utils import console, redir, telemetry
 from reflex.utils.exec import should_use_granian
 
-# Disable typer+rich integration for help panels
-typer.core.rich = None  # pyright: ignore [reportPrivateImportUsage]
 
-# Create the app.
-cli = typer.Typer(add_completion=False, pretty_exceptions_enable=False)
-
-
-def version(value: bool):
-    """Get the Reflex version.
+def set_loglevel(ctx: click.Context, self: click.Parameter, value: str | None):
+    """Set the log level.
 
     Args:
-        value: Whether the version flag was passed.
-
-    Raises:
-        typer.Exit: If the version flag was passed.
+        ctx: The click context.
+        self: The click command.
+        value: The log level to set.
     """
-    if value:
-        console.print(constants.Reflex.VERSION)
-        raise typer.Exit()
-
-
-@cli.callback()
-def main(
-    version: bool = typer.Option(
-        None,
-        "-v",
-        "--version",
-        callback=version,
-        help="Get the Reflex version.",
-        is_eager=True,
-    ),
-):
+    if value is not None:
+        loglevel = constants.LogLevel.from_string(value)
+        console.set_log_level(loglevel)
+
+
+@click.group
+@click.version_option(constants.Reflex.VERSION, message="%(version)s")
+def cli():
     """Reflex CLI to create, run, and deploy apps."""
     pass
 
 
+loglevel_option = click.option(
+    "--loglevel",
+    type=click.Choice(
+        [loglevel.value for loglevel in constants.LogLevel],
+        case_sensitive=False,
+    ),
+    is_eager=True,
+    callback=set_loglevel,
+    expose_value=False,
+    help="The log level to use.",
+)
+
+
 def _init(
     name: str,
     template: str | None = None,
-    loglevel: constants.LogLevel | None = None,
     ai: bool = False,
 ):
     """Initialize a new Reflex app in the given directory."""
     from reflex.utils import exec, prerequisites
 
-    if loglevel is not None:
-        console.set_log_level(loglevel)
-
-    config = get_config()
-
-    # Set the log level.
-    loglevel = loglevel or config.loglevel
-    console.set_log_level(loglevel)
-
     # Show system info
     exec.output_system_info()
 
@@ -97,7 +86,7 @@ def _init(
     prerequisites.initialize_gitignore()
 
     # Initialize the requirements.txt.
-    wrote_to_requirements = prerequisites.initialize_requirements_txt()
+    needs_user_manual_update = prerequisites.initialize_requirements_txt()
 
     template_msg = f" using the {template} template" if template else ""
     # Finish initializing the app.
@@ -105,31 +94,35 @@ def _init(
         f"Initialized {app_name}{template_msg}."
         + (
             f" Make sure to add {constants.RequirementsTxt.DEFAULTS_STUB + constants.Reflex.VERSION} to your requirements.txt or pyproject.toml file."
-            if not wrote_to_requirements
+            if needs_user_manual_update
             else ""
         )
     )
 
 
 @cli.command()
+@loglevel_option
+@click.option(
+    "--name",
+    metavar="APP_NAME",
+    help="The name of the app to initialize.",
+)
+@click.option(
+    "--template",
+    help="The template to initialize the app with.",
+)
+@click.option(
+    "--ai",
+    is_flag=True,
+    help="Use AI to create the initial template. Cannot be used with existing app or `--template` option.",
+)
 def init(
-    name: str = typer.Option(
-        None, metavar="APP_NAME", help="The name of the app to initialize."
-    ),
-    template: str = typer.Option(
-        None,
-        help="The template to initialize the app with.",
-    ),
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
-    ai: bool = typer.Option(
-        False,
-        help="Use AI to create the initial template. Cannot be used with existing app or `--template` option.",
-    ),
+    name: str,
+    template: str | None,
+    ai: bool,
 ):
     """Initialize a new Reflex app in the current directory."""
-    _init(name, template, loglevel, ai)
+    _init(name, template, ai)
 
 
 def _run(
@@ -139,22 +132,14 @@ def _run(
     frontend_port: int | None = None,
     backend_port: int | None = None,
     backend_host: str | None = None,
-    loglevel: constants.LogLevel | None = None,
 ):
     """Run the app in the given directory."""
     from reflex.utils import build, exec, prerequisites, processes
 
-    if loglevel is not None:
-        console.set_log_level(loglevel)
-
     config = get_config()
 
-    loglevel = loglevel or config.loglevel
     backend_host = backend_host or config.backend_host
 
-    # Set the log level.
-    console.set_log_level(loglevel)
-
     # Set env mode in the environment
     environment.REFLEX_ENV_MODE.set(env)
 
@@ -166,9 +151,11 @@ def _run(
     if not frontend and backend:
         _skip_compile()
 
+    prerequisites.assert_in_reflex_dir()
+
     # Check that the app is initialized.
-    if prerequisites.needs_reinit(frontend=frontend):
-        _init(name=config.app_name, loglevel=loglevel)
+    if frontend and prerequisites.needs_reinit():
+        _init(name=config.app_name)
 
     # Delete the states folder if it exists.
     reset_disk_state_manager()
@@ -228,7 +215,7 @@ def _run(
     else:
         validation_result = app_task(*args)
     if not validation_result:
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
 
     # Warn if schema is not up to date.
     prerequisites.check_schema_up_to_date()
@@ -248,7 +235,7 @@ def _run(
             exec.run_backend_prod,
         )
     if not setup_frontend or not frontend_cmd or not backend_cmd:
-        raise ValueError("Invalid env")
+        raise ValueError(f"Invalid env: {env}. Must be DEV or PROD.")
 
     # Post a telemetry event.
     telemetry.send(f"run-{env.value}")
@@ -271,7 +258,7 @@ def _run(
                 backend_cmd,
                 backend_host,
                 backend_port,
-                loglevel.subprocess_level(),
+                config.loglevel.subprocess_level(),
                 frontend,
             )
         )
@@ -281,7 +268,10 @@ def _run(
         # In dev mode, run the backend on the main thread.
         if backend and backend_port and env == constants.Env.DEV:
             backend_cmd(
-                backend_host, int(backend_port), loglevel.subprocess_level(), frontend
+                backend_host,
+                int(backend_port),
+                config.loglevel.subprocess_level(),
+                frontend,
             )
             # The windows uvicorn bug workaround
             # https://github.com/reflex-dev/reflex/issues/2335
@@ -291,94 +281,123 @@ def _run(
 
 
 @cli.command()
+@loglevel_option
+@click.option(
+    "--env",
+    type=click.Choice([e.value for e in constants.Env], case_sensitive=False),
+    default=constants.Env.DEV.value,
+    help="The environment to run the app in.",
+)
+@click.option(
+    "--frontend-only",
+    is_flag=True,
+    show_default=False,
+    help="Execute only frontend.",
+    envvar=environment.REFLEX_FRONTEND_ONLY.name,
+)
+@click.option(
+    "--backend-only",
+    is_flag=True,
+    show_default=False,
+    help="Execute only backend.",
+    envvar=environment.REFLEX_BACKEND_ONLY.name,
+)
+@click.option(
+    "--frontend-port",
+    type=int,
+    help="Specify a different frontend port.",
+    envvar=environment.REFLEX_FRONTEND_PORT.name,
+)
+@click.option(
+    "--backend-port",
+    type=int,
+    help="Specify a different backend port.",
+    envvar=environment.REFLEX_BACKEND_PORT.name,
+)
+@click.option(
+    "--backend-host",
+    help="Specify the backend host.",
+)
 def run(
-    env: constants.Env = typer.Option(
-        constants.Env.DEV, help="The environment to run the app in."
-    ),
-    frontend: bool = typer.Option(
-        False,
-        "--frontend-only",
-        help="Execute only frontend.",
-        envvar=environment.REFLEX_FRONTEND_ONLY.name,
-    ),
-    backend: bool = typer.Option(
-        False,
-        "--backend-only",
-        help="Execute only backend.",
-        envvar=environment.REFLEX_BACKEND_ONLY.name,
-    ),
-    frontend_port: int | None = typer.Option(
-        None,
-        help="Specify a different frontend port.",
-        envvar=environment.REFLEX_FRONTEND_PORT.name,
-    ),
-    backend_port: int | None = typer.Option(
-        None,
-        help="Specify a different backend port.",
-        envvar=environment.REFLEX_BACKEND_PORT.name,
-    ),
-    backend_host: str | None = typer.Option(None, help="Specify the backend host."),
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
+    env: LITERAL_ENV,
+    frontend_only: bool,
+    backend_only: bool,
+    frontend_port: int | None,
+    backend_port: int | None,
+    backend_host: str | None,
 ):
     """Run the app in the current directory."""
-    if frontend and backend:
+    if frontend_only and backend_only:
         console.error("Cannot use both --frontend-only and --backend-only options.")
-        raise typer.Exit(1)
-
-    if loglevel is not None:
-        console.set_log_level(loglevel)
+        raise click.exceptions.Exit(1)
 
     config = get_config()
 
     frontend_port = frontend_port or config.frontend_port
     backend_port = backend_port or config.backend_port
     backend_host = backend_host or config.backend_host
-    loglevel = loglevel or config.loglevel
 
     environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.RUN)
-    environment.REFLEX_BACKEND_ONLY.set(backend)
-    environment.REFLEX_FRONTEND_ONLY.set(frontend)
-
-    _run(env, frontend, backend, frontend_port, backend_port, backend_host, loglevel)
+    environment.REFLEX_BACKEND_ONLY.set(backend_only)
+    environment.REFLEX_FRONTEND_ONLY.set(frontend_only)
+
+    _run(
+        constants.Env.DEV if env == constants.Env.DEV else constants.Env.PROD,
+        frontend_only,
+        backend_only,
+        frontend_port,
+        backend_port,
+        backend_host,
+    )
 
 
 @cli.command()
+@loglevel_option
+@click.option(
+    "--zip/--no-zip",
+    default=True,
+    is_flag=True,
+    help="Whether to zip the backend and frontend exports.",
+)
+@click.option(
+    "--frontend-only",
+    is_flag=True,
+    show_default=False,
+    envvar=environment.REFLEX_FRONTEND_ONLY.name,
+    help="Export only frontend.",
+)
+@click.option(
+    "--backend-only",
+    is_flag=True,
+    show_default=False,
+    envvar=environment.REFLEX_BACKEND_ONLY.name,
+    help="Export only backend.",
+)
+@click.option(
+    "--zip-dest-dir",
+    default=str(Path.cwd()),
+    help="The directory to export the zip files to.",
+    show_default=False,
+)
+@click.option(
+    "--upload-db-file",
+    is_flag=True,
+    help="Whether to exclude sqlite db files when exporting backend.",
+    hidden=True,
+)
+@click.option(
+    "--env",
+    type=click.Choice([e.value for e in constants.Env], case_sensitive=False),
+    default=constants.Env.PROD.value,
+    help="The environment to export the app in.",
+)
 def export(
-    zipping: bool = typer.Option(
-        True, "--no-zip", help="Disable zip for backend and frontend exports."
-    ),
-    frontend: bool = typer.Option(
-        False,
-        "--frontend-only",
-        help="Export only frontend.",
-        show_default=False,
-        envvar=environment.REFLEX_FRONTEND_ONLY.name,
-    ),
-    backend: bool = typer.Option(
-        False,
-        "--backend-only",
-        help="Export only backend.",
-        show_default=False,
-        envvar=environment.REFLEX_BACKEND_ONLY.name,
-    ),
-    zip_dest_dir: str = typer.Option(
-        str(Path.cwd()),
-        help="The directory to export the zip files to.",
-        show_default=False,
-    ),
-    upload_db_file: bool = typer.Option(
-        False,
-        help="Whether to exclude sqlite db files when exporting backend.",
-        hidden=True,
-    ),
-    env: constants.Env = typer.Option(
-        constants.Env.PROD, help="The environment to export the app in."
-    ),
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
+    zip: bool,
+    frontend_only: bool,
+    backend_only: bool,
+    zip_dest_dir: str,
+    upload_db_file: bool,
+    env: LITERAL_ENV,
 ):
     """Export the app to a zip file."""
     from reflex.utils import export as export_utils
@@ -386,35 +405,35 @@ def export(
 
     environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.EXPORT)
 
-    frontend, backend = prerequisites.check_running_mode(frontend, backend)
+    should_frontend_run, should_backend_run = prerequisites.check_running_mode(
+        frontend_only, backend_only
+    )
 
-    loglevel = loglevel or get_config().loglevel
-    console.set_log_level(loglevel)
+    config = get_config()
+
+    prerequisites.assert_in_reflex_dir()
 
-    if prerequisites.needs_reinit(frontend=frontend or not backend):
-        _init(name=get_config().app_name, loglevel=loglevel)
+    if should_frontend_run and prerequisites.needs_reinit():
+        _init(name=config.app_name)
 
     export_utils.export(
-        zipping=zipping,
-        frontend=frontend,
-        backend=backend,
+        zipping=zip,
+        frontend=should_frontend_run,
+        backend=should_backend_run,
         zip_dest_dir=zip_dest_dir,
         upload_db_file=upload_db_file,
-        env=env,
-        loglevel=loglevel.subprocess_level(),
+        env=constants.Env.DEV if env == constants.Env.DEV else constants.Env.PROD,
+        loglevel=config.loglevel.subprocess_level(),
     )
 
 
 @cli.command()
-def login(loglevel: constants.LogLevel | None = typer.Option(None)):
+@loglevel_option
+def login():
     """Authenticate with experimental Reflex hosting service."""
     from reflex_cli.v2 import cli as hosting_cli
     from reflex_cli.v2.deployments import check_version
 
-    loglevel = loglevel or get_config().loglevel
-
-    console.set_log_level(loglevel)
-
     check_version()
 
     validated_info = hosting_cli.login()
@@ -424,24 +443,27 @@ def login(loglevel: constants.LogLevel | None = typer.Option(None)):
 
 
 @cli.command()
-def logout(
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
-):
+@loglevel_option
+def logout():
     """Log out of access to Reflex hosting service."""
     from reflex_cli.v2.cli import logout
     from reflex_cli.v2.deployments import check_version
 
     check_version()
 
-    loglevel = loglevel or get_config().loglevel
+    logout(_convert_reflex_loglevel_to_reflex_cli_loglevel(get_config().loglevel))
+
 
-    logout(_convert_reflex_loglevel_to_reflex_cli_loglevel(loglevel))
+@click.group
+def db_cli():
+    """Subcommands for managing the database schema."""
+    pass
 
 
-db_cli = typer.Typer()
-script_cli = typer.Typer()
+@click.group
+def script_cli():
+    """Subcommands for running helper scripts."""
+    pass
 
 
 def _skip_compile():
@@ -495,11 +517,11 @@ def migrate():
 
 
 @db_cli.command()
-def makemigrations(
-    message: str = typer.Option(
-        None, help="Human readable identifier for the generated revision."
-    ),
-):
+@click.option(
+    "--message",
+    help="Human readable identifier for the generated revision.",
+)
+def makemigrations(message: str | None):
     """Create autogenerated alembic migration scripts."""
     from alembic.util.exc import CommandError
 
@@ -523,70 +545,74 @@ def makemigrations(
 
 
 @cli.command()
+@loglevel_option
+@click.option(
+    "--app-name",
+    help="The name of the app to deploy.",
+)
+@click.option(
+    "--app-id",
+    help="The ID of the app to deploy.",
+)
+@click.option(
+    "-r",
+    "--region",
+    multiple=True,
+    help="The regions to deploy to. `reflex cloud regions` For multiple envs, repeat this option, e.g. --region sjc --region iad",
+)
+@click.option(
+    "--env",
+    multiple=True,
+    help="The environment variables to set: <key>=<value>. For multiple envs, repeat this option, e.g. --env k1=v2 --env k2=v2.",
+)
+@click.option(
+    "--vmtype",
+    help="Vm type id. Run `reflex cloud vmtypes` to get options.",
+)
+@click.option(
+    "--hostname",
+    help="The hostname of the frontend.",
+)
+@click.option(
+    "--interactive/--no-interactive",
+    is_flag=True,
+    default=True,
+    help="Whether to list configuration options and ask for confirmation.",
+)
+@click.option(
+    "--envfile",
+    help="The path to an env file to use. Will override any envs set manually.",
+)
+@click.option(
+    "--project",
+    help="project id to deploy to",
+)
+@click.option(
+    "--project-name",
+    help="The name of the project to deploy to.",
+)
+@click.option(
+    "--token",
+    help="token to use for auth",
+)
+@click.option(
+    "--config-path",
+    "--config",
+    help="path to the config file",
+)
 def deploy(
-    app_name: str | None = typer.Option(
-        None,
-        "--app-name",
-        help="The name of the App to deploy under.",
-    ),
-    app_id: str = typer.Option(
-        None,
-        "--app-id",
-        help="The ID of the App to deploy over.",
-    ),
-    regions: list[str] = typer.Option(
-        [],
-        "-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(
-        [],
-        "--env",
-        help="The environment variables to set: <key>=<value>. For multiple envs, repeat this option, e.g. --env k1=v2 --env k2=v2.",
-    ),
-    vmtype: str | None = typer.Option(
-        None,
-        "--vmtype",
-        help="Vm type id. Run `reflex cloud vmtypes` to get options.",
-    ),
-    hostname: str | None = typer.Option(
-        None,
-        "--hostname",
-        help="The hostname of the frontend.",
-    ),
-    interactive: bool = typer.Option(
-        True,
-        help="Whether to list configuration options and ask for confirmation.",
-    ),
-    envfile: str | None = typer.Option(
-        None,
-        "--envfile",
-        help="The path to an env file to use. Will override any envs set manually.",
-    ),
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
-    project: str | None = typer.Option(
-        None,
-        "--project",
-        help="project id to deploy to",
-    ),
-    project_name: str | None = typer.Option(
-        None,
-        "--project-name",
-        help="The name of the project to deploy to.",
-    ),
-    token: str | None = typer.Option(
-        None,
-        "--token",
-        help="token to use for auth",
-    ),
-    config_path: str | None = typer.Option(
-        None,
-        "--config",
-        help="path to the config file",
-    ),
+    app_name: str | None,
+    app_id: str | None,
+    region: tuple[str, ...],
+    env: tuple[str],
+    vmtype: str | None,
+    hostname: str | None,
+    interactive: bool,
+    envfile: str | None,
+    project: str | None,
+    project_name: str | None,
+    token: str | None,
+    config_path: str | None,
 ):
     """Deploy the app to the Reflex hosting service."""
     from reflex_cli.utils import dependency
@@ -596,29 +622,24 @@ def deploy(
     from reflex.utils import export as export_utils
     from reflex.utils import prerequisites
 
-    if loglevel is not None:
-        console.set_log_level(loglevel)
-
     config = get_config()
 
-    loglevel = loglevel or config.loglevel
     app_name = app_name or config.app_name
 
     check_version()
 
     environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.DEPLOY)
 
-    # Set the log level.
-    console.set_log_level(loglevel)
-
     # Only check requirements if interactive.
     # There is user interaction for requirements update.
     if interactive:
         dependency.check_requirements()
 
+    prerequisites.assert_in_reflex_dir()
+
     # Check if we are set up.
-    if prerequisites.needs_reinit(frontend=True):
-        _init(name=config.app_name, loglevel=loglevel)
+    if prerequisites.needs_reinit():
+        _init(name=config.app_name)
     prerequisites.check_latest_package_version(constants.ReflexHostingCLI.MODULE_NAME)
 
     hosting_cli.deploy(
@@ -638,17 +659,17 @@ def deploy(
                 frontend=frontend,
                 backend=backend,
                 zipping=zipping,
-                loglevel=loglevel.subprocess_level(),
+                loglevel=config.loglevel.subprocess_level(),
                 upload_db_file=upload_db,
             )
         ),
-        regions=regions,
-        envs=envs,
+        regions=list(region),
+        envs=list(env),
         vmtype=vmtype,
         envfile=envfile,
         hostname=hostname,
         interactive=interactive,
-        loglevel=_convert_reflex_loglevel_to_reflex_cli_loglevel(loglevel),
+        loglevel=_convert_reflex_loglevel_to_reflex_cli_loglevel(config.loglevel),
         token=token,
         project=project,
         project_name=project_name,
@@ -657,19 +678,14 @@ def deploy(
 
 
 @cli.command()
-def rename(
-    new_name: str = typer.Argument(..., help="The new name for the app."),
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
-):
+@loglevel_option
+@click.argument("new_name")
+def rename(new_name: str):
     """Rename the app in the current directory."""
     from reflex.utils import prerequisites
 
-    loglevel = loglevel or get_config().loglevel
-
     prerequisites.validate_app_name(new_name)
-    prerequisites.rename_app(new_name, loglevel)
+    prerequisites.rename_app(new_name, get_config().loglevel)
 
 
 if TYPE_CHECKING:
@@ -702,18 +718,20 @@ def _convert_reflex_loglevel_to_reflex_cli_loglevel(
     return HostingLogLevel.INFO
 
 
-cli.add_typer(db_cli, name="db", help="Subcommands for managing the database schema.")
-cli.add_typer(script_cli, name="script", help="Subcommands running helper scripts.")
-cli.add_typer(
-    hosting_cli,
-    name="cloud",
-    help="Subcommands for managing the reflex cloud.",
-)
-cli.add_typer(
-    custom_components_cli,
-    name="component",
-    help="Subcommands for creating and publishing Custom Components.",
-)
+if find_spec("typer") and find_spec("typer.main"):
+    import typer  # pyright: ignore[reportMissingImports]
+
+    if isinstance(hosting_cli, typer.Typer):
+        hosting_cli_command = typer.main.get_command(hosting_cli)
+    else:
+        hosting_cli_command = hosting_cli
+else:
+    hosting_cli_command = hosting_cli
+
+cli.add_command(hosting_cli_command, name="cloud")
+cli.add_command(db_cli, name="db")
+cli.add_command(script_cli, name="script")
+cli.add_command(custom_components_cli, name="component")
 
 if __name__ == "__main__":
     cli()

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov