Bladeren bron

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

Khaleel Al-Adhami 4 maanden geleden
bovenliggende
commit
d31510c655
50 gewijzigde bestanden met toevoegingen van 1273 en 484 verwijderingen
  1. 21 21
      .github/workflows/benchmarks.yml
  2. 5 5
      .github/workflows/check_generated_pyi.yml
  3. 32 35
      .github/workflows/check_node_latest.yml
  4. 64 66
      .github/workflows/check_outdated_dependencies.yml
  5. 3 3
      .github/workflows/integration_app_harness.yml
  6. 21 19
      .github/workflows/integration_tests.yml
  7. 3 3
      .github/workflows/pre-commit.yml
  8. 16 12
      .github/workflows/unit_tests.yml
  9. 1 1
      benchmarks/benchmark_package_size.py
  10. 4 5
      pyproject.toml
  11. 2 4
      reflex/.templates/jinja/web/pages/_app.js.jinja2
  12. 3 4
      reflex/.templates/jinja/web/pages/custom_component.js.jinja2
  13. 2 3
      reflex/.templates/jinja/web/pages/index.js.jinja2
  14. 38 0
      reflex/.templates/jinja/web/pages/macros.js.jinja2
  15. 4 16
      reflex/.templates/jinja/web/pages/stateful_component.js.jinja2
  16. 9 4
      reflex/.templates/web/utils/state.js
  17. 2 3
      reflex/app.py
  18. 2 2
      reflex/compiler/compiler.py
  19. 41 0
      reflex/compiler/templates.py
  20. 1 1
      reflex/compiler/utils.py
  21. 7 3
      reflex/components/base/bare.py
  22. 78 116
      reflex/components/component.py
  23. 13 9
      reflex/components/datadisplay/code.py
  24. 7 5
      reflex/components/el/elements/forms.py
  25. 155 20
      reflex/components/lucide/icon.py
  26. 145 18
      reflex/components/lucide/icon.pyi
  27. 3 2
      reflex/components/markdown/markdown.py
  28. 2 2
      reflex/components/radix/themes/color_mode.py
  29. 1 1
      reflex/components/radix/themes/typography/link.py
  30. 2 2
      reflex/components/recharts/charts.py
  31. 1 1
      reflex/components/sonner/toast.py
  32. 1 0
      reflex/constants/compiler.py
  33. 98 1
      reflex/event.py
  34. 42 20
      reflex/experimental/client_state.py
  35. 1 0
      reflex/model.py
  36. 6 0
      reflex/reflex.py
  37. 107 2
      reflex/state.py
  38. 3 3
      reflex/testing.py
  39. 4 0
      reflex/utils/exceptions.py
  40. 13 11
      reflex/utils/prerequisites.py
  41. 1 1
      reflex/utils/pyi_generator.py
  42. 15 4
      reflex/vars/base.py
  43. 16 0
      reflex/vars/sequence.py
  44. 3 3
      scripts/wait_for_listening_port.py
  45. 12 1
      tests/integration/test_lifespan.py
  46. 129 31
      tests/integration/test_upload.py
  47. 46 0
      tests/integration/tests_playwright/test_link_hover.py
  48. 8 2
      tests/units/components/lucide/test_icon.py
  49. 14 4
      tests/units/test_event.py
  50. 66 15
      tests/units/test_state.py

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

@@ -5,7 +5,7 @@ on:
     types:
     types:
       - closed
       - closed
     paths-ignore:
     paths-ignore:
-      - '**/*.md'
+      - "**/*.md"
 
 
 permissions:
 permissions:
   contents: read
   contents: read
@@ -15,21 +15,21 @@ defaults:
     shell: bash
     shell: bash
 
 
 env:
 env:
-  PYTHONIOENCODING: 'utf8'
+  PYTHONIOENCODING: "utf8"
   TELEMETRY_ENABLED: false
   TELEMETRY_ENABLED: false
-  NODE_OPTIONS: '--max_old_space_size=8192'
+  NODE_OPTIONS: "--max_old_space_size=8192"
   PR_TITLE: ${{ github.event.pull_request.title }}
   PR_TITLE: ${{ github.event.pull_request.title }}
 
 
 jobs:
 jobs:
   reflex-web:
   reflex-web:
-#    if: github.event.pull_request.merged == true
+    #    if: github.event.pull_request.merged == true
     strategy:
     strategy:
       fail-fast: false
       fail-fast: false
       matrix:
       matrix:
         # Show OS combos first in GUI
         # Show OS combos first in GUI
         os: [ubuntu-latest]
         os: [ubuntu-latest]
-        python-version: ['3.11.4']
-        node-version: ['18.x']
+        python-version: ["3.12.8"]
+        node-version: ["18.x"]
 
 
     runs-on: ${{ matrix.os }}
     runs-on: ${{ matrix.os }}
     steps:
     steps:
@@ -81,24 +81,24 @@ jobs:
       matrix:
       matrix:
         # Show OS combos first in GUI
         # Show OS combos first in GUI
         os: [ubuntu-latest, windows-latest, macos-latest]
         os: [ubuntu-latest, windows-latest, macos-latest]
-        python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0']
+        python-version: ["3.9.21", "3.10.16", "3.11.11", "3.12.8"]
         exclude:
         exclude:
           - os: windows-latest
           - os: windows-latest
-            python-version: '3.10.13'
+            python-version: "3.10.16"
           - os: windows-latest
           - os: windows-latest
-            python-version: '3.9.18'
+            python-version: "3.9.21"
           # keep only one python version for MacOS
           # keep only one python version for MacOS
           - os: macos-latest
           - os: macos-latest
-            python-version: '3.9.18'
+            python-version: "3.9.21"
           - os: macos-latest
           - os: macos-latest
-            python-version: '3.10.13'
+            python-version: "3.10.16"
           - os: macos-latest
           - os: macos-latest
-            python-version: '3.12.0'
+            python-version: "3.11.11"
         include:
         include:
           - os: windows-latest
           - os: windows-latest
-            python-version: '3.10.11'
+            python-version: "3.10.11"
           - os: windows-latest
           - os: windows-latest
-            python-version: '3.9.13'
+            python-version: "3.9.13"
 
 
     runs-on: ${{ matrix.os }}
     runs-on: ${{ matrix.os }}
     steps:
     steps:
@@ -123,7 +123,7 @@ jobs:
           --event-type "${{ github.event_name }}" --pr-id "${{ github.event.pull_request.id }}"
           --event-type "${{ github.event_name }}" --pr-id "${{ github.event.pull_request.id }}"
 
 
   reflex-dist-size: # This job is used to calculate the size of the Reflex distribution (wheel file)
   reflex-dist-size: # This job is used to calculate the size of the Reflex distribution (wheel file)
-    if: github.event.pull_request.merged == true  
+    if: github.event.pull_request.merged == true
     timeout-minutes: 30
     timeout-minutes: 30
     strategy:
     strategy:
       # Prioritize getting more information out of the workflow (even if something fails)
       # Prioritize getting more information out of the workflow (even if something fails)
@@ -133,7 +133,7 @@ jobs:
       - uses: actions/checkout@v4
       - uses: actions/checkout@v4
       - uses: ./.github/actions/setup_build_env
       - uses: ./.github/actions/setup_build_env
         with:
         with:
-          python-version: 3.11.5
+          python-version: 3.12.8
           run-poetry-install: true
           run-poetry-install: true
           create-venv-at-path: .venv
           create-venv-at-path: .venv
       - name: Build reflex
       - name: Build reflex
@@ -143,12 +143,12 @@ jobs:
         # Only run if the database creds are available in this context.
         # Only run if the database creds are available in this context.
         run:
         run:
           poetry run python benchmarks/benchmark_package_size.py --os ubuntu-latest
           poetry run python benchmarks/benchmark_package_size.py --os ubuntu-latest
-          --python-version 3.11.5 --commit-sha "${{ github.sha }}" --pr-id "${{ github.event.pull_request.id }}"
+          --python-version 3.12.8 --commit-sha "${{ github.sha }}" --pr-id "${{ github.event.pull_request.id }}"
           --branch-name "${{ github.head_ref || github.ref_name }}"
           --branch-name "${{ github.head_ref || github.ref_name }}"
           --path ./dist
           --path ./dist
 
 
   reflex-venv-size: # This job calculates the total size of Reflex and its dependencies
   reflex-venv-size: # This job calculates the total size of Reflex and its dependencies
-    if: github.event.pull_request.merged == true  
+    if: github.event.pull_request.merged == true
     timeout-minutes: 30
     timeout-minutes: 30
     strategy:
     strategy:
       # Prioritize getting more information out of the workflow (even if something fails)
       # Prioritize getting more information out of the workflow (even if something fails)
@@ -156,7 +156,7 @@ jobs:
       matrix:
       matrix:
         # Show OS combos first in GUI
         # Show OS combos first in GUI
         os: [ubuntu-latest, windows-latest, macos-latest]
         os: [ubuntu-latest, windows-latest, macos-latest]
-        python-version: ['3.11.5']
+        python-version: ["3.12.8"]
 
 
     runs-on: ${{ matrix.os }}
     runs-on: ${{ matrix.os }}
     steps:
     steps:
@@ -186,6 +186,6 @@ jobs:
         run:
         run:
           poetry run python benchmarks/benchmark_package_size.py --os "${{ matrix.os }}"
           poetry run python benchmarks/benchmark_package_size.py --os "${{ matrix.os }}"
           --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}"
           --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}"
-          --pr-id "${{ github.event.pull_request.id }}" 
+          --pr-id "${{ github.event.pull_request.id }}"
           --branch-name "${{ github.head_ref || github.ref_name }}"
           --branch-name "${{ github.head_ref || github.ref_name }}"
-          --path ./.venv
+          --path ./.venv

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

@@ -6,16 +6,16 @@ concurrency:
 
 
 on:
 on:
   push:
   push:
-    branches: ['main']
+    branches: ["main"]
     # We don't just trigger on make_pyi.py and the components dir, because
     # We don't just trigger on make_pyi.py and the components dir, because
     # there are other things that can change the generator output
     # there are other things that can change the generator output
     # e.g. black version, reflex.Component, reflex.Var.
     # e.g. black version, reflex.Component, reflex.Var.
     paths-ignore:
     paths-ignore:
-      - '**/*.md'
+      - "**/*.md"
   pull_request:
   pull_request:
-    branches: ['main']
+    branches: ["main"]
     paths-ignore:
     paths-ignore:
-      - '**/*.md'
+      - "**/*.md"
 
 
 jobs:
 jobs:
   check-generated-pyi-components:
   check-generated-pyi-components:
@@ -25,7 +25,7 @@ jobs:
       - uses: actions/checkout@v4
       - uses: actions/checkout@v4
       - uses: ./.github/actions/setup_build_env
       - uses: ./.github/actions/setup_build_env
         with:
         with:
-          python-version: '3.11.5'
+          python-version: "3.12.8"
           run-poetry-install: true
           run-poetry-install: true
           create-venv-at-path: .venv
           create-venv-at-path: .venv
       - run: |
       - run: |

+ 32 - 35
.github/workflows/check_node_latest.yml

@@ -1,43 +1,40 @@
 name: integration-node-latest
 name: integration-node-latest
 
 
 on:
 on:
-    push:
-        branches:
-            - main
-    pull_request:
-        branches:
-            - main
+  push:
+    branches:
+      - main
+  pull_request:
+    branches:
+      - main
 
 
 env:
 env:
-    TELEMETRY_ENABLED: false
-    REFLEX_USE_SYSTEM_NODE: true
+  TELEMETRY_ENABLED: false
+  REFLEX_USE_SYSTEM_NODE: true
 
 
 jobs:
 jobs:
-    check_latest_node:
-        runs-on: ubuntu-22.04
-        strategy:
-            matrix:
-                python-version: ['3.12']
-                split_index: [1, 2]
-                node-version: ['node']
-            fail-fast: false
-
-        steps:
-            - uses: actions/checkout@v4
-            - uses: ./.github/actions/setup_build_env
-              with:
-                python-version: ${{ matrix.python-version }}
-                run-poetry-install: true
-                create-venv-at-path: .venv
-            - uses: actions/setup-node@v4
-              with:
-                node-version: ${{ matrix.node-version }}
-            - run: |
-                poetry run uv pip install pyvirtualdisplay pillow pytest-split
-                poetry run playwright install --with-deps
-            - run: |
-                poetry run pytest tests/test_node_version.py
-                poetry run pytest tests/integration --splits 2 --group ${{matrix.split_index}}
-  
-
+  check_latest_node:
+    runs-on: ubuntu-22.04
+    strategy:
+      matrix:
+        python-version: ["3.12.8"]
+        split_index: [1, 2]
+        node-version: ["node"]
+      fail-fast: false
 
 
+    steps:
+      - uses: actions/checkout@v4
+      - uses: ./.github/actions/setup_build_env
+        with:
+          python-version: ${{ matrix.python-version }}
+          run-poetry-install: true
+          create-venv-at-path: .venv
+      - uses: actions/setup-node@v4
+        with:
+          node-version: ${{ matrix.node-version }}
+      - run: |
+          poetry run uv pip install pyvirtualdisplay pillow pytest-split
+          poetry run playwright install --with-deps
+      - run: |
+          poetry run pytest tests/test_node_version.py
+          poetry run pytest tests/integration --splits 2 --group ${{matrix.split_index}}

+ 64 - 66
.github/workflows/check_outdated_dependencies.yml

@@ -1,88 +1,86 @@
 name: check-outdated-dependencies
 name: check-outdated-dependencies
 
 
 on:
 on:
-  push:  # This will trigger the action when a pull request is opened or updated.
+  push: # This will trigger the action when a pull request is opened or updated.
     branches:
     branches:
-      - 'release/**'  # This will trigger the action when any branch starting with "release/" is created.
-  workflow_dispatch:  # Allow manual triggering if needed.
+      - "release/**" # This will trigger the action when any branch starting with "release/" is created.
+  workflow_dispatch: # Allow manual triggering if needed.
 
 
 jobs:
 jobs:
   backend:
   backend:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
 
 
     steps:
     steps:
-    - name: Checkout code
-      uses: actions/checkout@v3
+      - name: Checkout code
+        uses: actions/checkout@v3
 
 
-    - uses: ./.github/actions/setup_build_env
-      with:
-        python-version: '3.9'
-        run-poetry-install: true
-        create-venv-at-path: .venv
+      - uses: ./.github/actions/setup_build_env
+        with:
+          python-version: "3.9.21"
+          run-poetry-install: true
+          create-venv-at-path: .venv
 
 
-    - name: Check outdated backend dependencies
-      run: |
-        outdated=$(poetry show -oT)
-        echo "Outdated:"
-        echo "$outdated"
+      - name: Check outdated backend dependencies
+        run: |
+          outdated=$(poetry show -oT)
+          echo "Outdated:"
+          echo "$outdated"
 
 
-        filtered_outdated=$(echo "$outdated" | grep -vE 'pyright|ruff' || true)
-
-        if [ ! -z "$filtered_outdated" ]; then
-          echo "Outdated dependencies found:"
-          echo "$filtered_outdated"
-          exit 1
-        else
-          echo "All dependencies are up to date. (pyright and ruff are ignored)"
-        fi
+          filtered_outdated=$(echo "$outdated" | grep -vE 'pyright|ruff' || true)
 
 
+          if [ ! -z "$filtered_outdated" ]; then
+            echo "Outdated dependencies found:"
+            echo "$filtered_outdated"
+            exit 1
+          else
+            echo "All dependencies are up to date. (pyright and ruff are ignored)"
+          fi
 
 
   frontend:
   frontend:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
 
 
     steps:
     steps:
-    - name: Checkout code
-      uses: actions/checkout@v4  
-    - uses: ./.github/actions/setup_build_env
-      with:
-        python-version: '3.10.11'
-        run-poetry-install: true
-        create-venv-at-path: .venv
-    - name: Clone Reflex Website Repo
-      uses: actions/checkout@v4
-      with:
-        repository: reflex-dev/reflex-web
-        ref: main
-        path: reflex-web
-    - name: Install Requirements for reflex-web
-      working-directory: ./reflex-web
-      run: poetry run uv pip install -r requirements.txt
-    - name: Install additional dependencies for DB access
-      run: poetry run uv pip install psycopg
-    - name: Init Website for reflex-web
-      working-directory: ./reflex-web
-      run: poetry run reflex init
-    - name: Run Website and Check for errors
-      run: |
-        poetry run bash scripts/integration.sh ./reflex-web dev
-    - name: Check outdated frontend dependencies
-      working-directory: ./reflex-web/.web
-      run: |
-        raw_outdated=$(/home/runner/.local/share/reflex/bun/bin/bun outdated)
-        outdated=$(echo "$raw_outdated" | grep -vE '\|\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\|' || true)
-        echo "Outdated:"
-        echo "$outdated"
-
-        # Ignore 3rd party dependencies that are not updated.
-        filtered_outdated=$(echo "$outdated" | grep -vE 'Package|@chakra-ui|lucide-react|@splinetool/runtime|ag-grid-react|framer-motion|react-markdown|remark-math|remark-gfm|rehype-katex|rehype-raw|remark-unwrap-images' || true)
-        no_extra=$(echo "$filtered_outdated" | grep -vE '\|\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-' || true)
+      - name: Checkout code
+        uses: actions/checkout@v4
+      - uses: ./.github/actions/setup_build_env
+        with:
+          python-version: "3.10.16"
+          run-poetry-install: true
+          create-venv-at-path: .venv
+      - name: Clone Reflex Website Repo
+        uses: actions/checkout@v4
+        with:
+          repository: reflex-dev/reflex-web
+          ref: main
+          path: reflex-web
+      - name: Install Requirements for reflex-web
+        working-directory: ./reflex-web
+        run: poetry run uv pip install -r requirements.txt
+      - name: Install additional dependencies for DB access
+        run: poetry run uv pip install psycopg
+      - name: Init Website for reflex-web
+        working-directory: ./reflex-web
+        run: poetry run reflex init
+      - name: Run Website and Check for errors
+        run: |
+          poetry run bash scripts/integration.sh ./reflex-web dev
+      - name: Check outdated frontend dependencies
+        working-directory: ./reflex-web/.web
+        run: |
+          raw_outdated=$(/home/runner/.local/share/reflex/bun/bin/bun outdated)
+          outdated=$(echo "$raw_outdated" | grep -vE '\|\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\|' || true)
+          echo "Outdated:"
+          echo "$outdated"
 
 
+          # Ignore 3rd party dependencies that are not updated.
+          filtered_outdated=$(echo "$outdated" | grep -vE 'Package|@chakra-ui|lucide-react|@splinetool/runtime|ag-grid-react|framer-motion|react-markdown|remark-math|remark-gfm|rehype-katex|rehype-raw|remark-unwrap-images' || true)
+          no_extra=$(echo "$filtered_outdated" | grep -vE '\|\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-' || true)
 
 
-        if [ ! -z "$no_extra" ]; then
-          echo "Outdated dependencies found:"
-          echo "$filtered_outdated"
-          exit 1
-        else
-          echo "All dependencies are up to date. (3rd party packages are ignored)"
-        fi
 
 
+          if [ ! -z "$no_extra" ]; then
+            echo "Outdated dependencies found:"
+            echo "$filtered_outdated"
+            exit 1
+          else
+            echo "All dependencies are up to date. (3rd party packages are ignored)"
+          fi

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

@@ -22,8 +22,8 @@ jobs:
     timeout-minutes: 30
     timeout-minutes: 30
     strategy:
     strategy:
       matrix:
       matrix:
-        state_manager: ['redis', 'memory']
-        python-version: ['3.11.5', '3.12.0', '3.13.0']
+        state_manager: ["redis", "memory"]
+        python-version: ["3.11.11", "3.12.8", "3.13.1"]
         split_index: [1, 2]
         split_index: [1, 2]
       fail-fast: false
       fail-fast: false
     runs-on: ubuntu-22.04
     runs-on: ubuntu-22.04
@@ -53,7 +53,7 @@ jobs:
           SCREENSHOT_DIR: /tmp/screenshots/${{ matrix.state_manager }}/${{ matrix.python-version }}/${{ matrix.split_index }}
           SCREENSHOT_DIR: /tmp/screenshots/${{ matrix.state_manager }}/${{ matrix.python-version }}/${{ matrix.split_index }}
           REDIS_URL: ${{ matrix.state_manager == 'redis' && 'redis://localhost:6379' || '' }}
           REDIS_URL: ${{ matrix.state_manager == 'redis' && 'redis://localhost:6379' || '' }}
         run: |
         run: |
-          poetry run playwright install --with-deps
+          poetry run playwright install chromium
           poetry run pytest tests/integration --splits 2 --group ${{matrix.split_index}}
           poetry run pytest tests/integration --splits 2 --group ${{matrix.split_index}}
       - uses: actions/upload-artifact@v4
       - uses: actions/upload-artifact@v4
         name: Upload failed test screenshots
         name: Upload failed test screenshots

+ 21 - 19
.github/workflows/integration_tests.yml

@@ -2,13 +2,13 @@ name: integration-tests
 
 
 on:
 on:
   push:
   push:
-    branches: ['main']
+    branches: ["main"]
     paths-ignore:
     paths-ignore:
-      - '**/*.md'
+      - "**/*.md"
   pull_request:
   pull_request:
-    branches: ['main']
+    branches: ["main"]
     paths-ignore:
     paths-ignore:
-      - '**/*.md'
+      - "**/*.md"
 
 
 concurrency:
 concurrency:
   group: ${{ github.workflow }}-${{ github.event.pull_request.id }}
   group: ${{ github.workflow }}-${{ github.event.pull_request.id }}
@@ -27,9 +27,9 @@ env:
   # TODO: can we fix windows encoding natively within reflex? Bug above can hit real users too (less common, but possible)
   # TODO: can we fix windows encoding natively within reflex? Bug above can hit real users too (less common, but possible)
   # - Catch encoding errors when printing logs
   # - Catch encoding errors when printing logs
   # - Best effort print lines that contain illegal chars (map to some default char, etc.)
   # - Best effort print lines that contain illegal chars (map to some default char, etc.)
-  PYTHONIOENCODING: 'utf8'
+  PYTHONIOENCODING: "utf8"
   TELEMETRY_ENABLED: false
   TELEMETRY_ENABLED: false
-  NODE_OPTIONS: '--max_old_space_size=8192'
+  NODE_OPTIONS: "--max_old_space_size=8192"
   PR_TITLE: ${{ github.event.pull_request.title }}
   PR_TITLE: ${{ github.event.pull_request.title }}
 
 
 jobs:
 jobs:
@@ -43,17 +43,22 @@ jobs:
       matrix:
       matrix:
         # Show OS combos first in GUI
         # Show OS combos first in GUI
         os: [ubuntu-latest, windows-latest]
         os: [ubuntu-latest, windows-latest]
-        python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0', '3.13.0']
+        python-version: ["3.9.21", "3.10.16", "3.11.11", "3.12.8", "3.13.1"]
+        # Windows is a bit behind on Python version availability in Github
         exclude:
         exclude:
           - os: windows-latest
           - os: windows-latest
-            python-version: '3.10.13'
+            python-version: "3.11.11"
           - os: windows-latest
           - os: windows-latest
-            python-version: '3.9.18'
+            python-version: "3.10.16"
+          - os: windows-latest
+            python-version: "3.9.21"
         include:
         include:
           - os: windows-latest
           - os: windows-latest
-            python-version: '3.10.11'
+            python-version: "3.11.9"
+          - os: windows-latest
+            python-version: "3.10.11"
           - os: windows-latest
           - os: windows-latest
-            python-version: '3.9.13'
+            python-version: "3.9.13"
 
 
     runs-on: ${{ matrix.os }}
     runs-on: ${{ matrix.os }}
     steps:
     steps:
@@ -115,18 +120,16 @@ jobs:
           --branch-name "${{ github.head_ref || github.ref_name }}" --pr-id "${{ github.event.pull_request.id }}"
           --branch-name "${{ github.head_ref || github.ref_name }}" --pr-id "${{ github.event.pull_request.id }}"
           --app-name "counter"
           --app-name "counter"
 
 
-
-
   reflex-web:
   reflex-web:
     strategy:
     strategy:
       fail-fast: false
       fail-fast: false
       matrix:
       matrix:
         # Show OS combos first in GUI
         # Show OS combos first in GUI
         os: [ubuntu-latest]
         os: [ubuntu-latest]
-        python-version: ['3.10.11', '3.11.4']
+        python-version: ["3.11.11", "3.12.8"]
 
 
     env:
     env:
-      REFLEX_WEB_WINDOWS_OVERRIDE: '1'
+      REFLEX_WEB_WINDOWS_OVERRIDE: "1"
     runs-on: ${{ matrix.os }}
     runs-on: ${{ matrix.os }}
     steps:
     steps:
       - uses: actions/checkout@v4
       - uses: actions/checkout@v4
@@ -171,7 +174,7 @@ jobs:
       - uses: actions/checkout@v4
       - uses: actions/checkout@v4
       - uses: ./.github/actions/setup_build_env
       - uses: ./.github/actions/setup_build_env
         with:
         with:
-          python-version: '3.11.4'
+          python-version: "3.11.11"
           run-poetry-install: true
           run-poetry-install: true
           create-venv-at-path: .venv
           create-venv-at-path: .venv
       - name: Create app directory
       - name: Create app directory
@@ -190,14 +193,14 @@ jobs:
           # Check that npm is home
           # Check that npm is home
           npm -v
           npm -v
           poetry run bash scripts/integration.sh ./rx-shout-from-template prod
           poetry run bash scripts/integration.sh ./rx-shout-from-template prod
-  
 
 
   reflex-web-macos:
   reflex-web-macos:
     if: github.event_name == 'push' && github.ref == 'refs/heads/main'
     if: github.event_name == 'push' && github.ref == 'refs/heads/main'
     strategy:
     strategy:
       fail-fast: false
       fail-fast: false
       matrix:
       matrix:
-        python-version: ['3.11.5', '3.12.0']
+        # Note: py311 version chosen due to available arm64 darwin builds.
+        python-version: ["3.11.9", "3.12.8"]
     runs-on: macos-latest
     runs-on: macos-latest
     steps:
     steps:
       - uses: actions/checkout@v4
       - uses: actions/checkout@v4
@@ -231,4 +234,3 @@ jobs:
           --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}"
           --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}"
           --pr-id "${{ github.event.pull_request.id }}" --branch-name "${{ github.head_ref || github.ref_name }}"
           --pr-id "${{ github.event.pull_request.id }}" --branch-name "${{ github.head_ref || github.ref_name }}"
           --app-name "reflex-web" --path ./reflex-web/.web
           --app-name "reflex-web" --path ./reflex-web/.web
-  

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

@@ -6,12 +6,12 @@ concurrency:
 
 
 on:
 on:
   pull_request:
   pull_request:
-    branches: ['main']
+    branches: ["main"]
   push:
   push:
     # Note even though this job is called "pre-commit" and runs "pre-commit", this job will run
     # Note even though this job is called "pre-commit" and runs "pre-commit", this job will run
     # also POST-commit on main also!  In case there are mishandled merge conflicts / bad auto-resolves
     # also POST-commit on main also!  In case there are mishandled merge conflicts / bad auto-resolves
     # when merging into main branch.
     # when merging into main branch.
-    branches: ['main']
+    branches: ["main"]
 
 
 jobs:
 jobs:
   pre-commit:
   pre-commit:
@@ -23,7 +23,7 @@ jobs:
         with:
         with:
           # running vs. one version of Python is OK
           # running vs. one version of Python is OK
           # i.e. ruff, black, etc.
           # i.e. ruff, black, etc.
-          python-version: 3.11.5
+          python-version: 3.12.8
           run-poetry-install: true
           run-poetry-install: true
           create-venv-at-path: .venv
           create-venv-at-path: .venv
       # TODO pre-commit related stuff can be cached too (not a bottleneck yet)
       # TODO pre-commit related stuff can be cached too (not a bottleneck yet)

+ 16 - 12
.github/workflows/unit_tests.yml

@@ -6,13 +6,13 @@ concurrency:
 
 
 on:
 on:
   push:
   push:
-    branches: ['main']
+    branches: ["main"]
     paths-ignore:
     paths-ignore:
-      - '**/*.md'
+      - "**/*.md"
   pull_request:
   pull_request:
-    branches: ['main']
+    branches: ["main"]
     paths-ignore:
     paths-ignore:
-      - '**/*.md'
+      - "**/*.md"
 
 
 permissions:
 permissions:
   contents: read
   contents: read
@@ -28,18 +28,22 @@ jobs:
       fail-fast: false
       fail-fast: false
       matrix:
       matrix:
         os: [ubuntu-latest, windows-latest]
         os: [ubuntu-latest, windows-latest]
-        python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0', '3.13.0']
+        python-version: ["3.9.21", "3.10.16", "3.11.11", "3.12.8", "3.13.1"]
         # Windows is a bit behind on Python version availability in Github
         # Windows is a bit behind on Python version availability in Github
         exclude:
         exclude:
           - os: windows-latest
           - os: windows-latest
-            python-version: '3.10.13'
+            python-version: "3.11.11"
           - os: windows-latest
           - os: windows-latest
-            python-version: '3.9.18'
+            python-version: "3.10.16"
+          - os: windows-latest
+            python-version: "3.9.21"
         include:
         include:
           - os: windows-latest
           - os: windows-latest
-            python-version: '3.10.11'
+            python-version: "3.11.9"
+          - os: windows-latest
+            python-version: "3.10.11"
           - os: windows-latest
           - os: windows-latest
-            python-version: '3.9.13'
+            python-version: "3.9.13"
     runs-on: ${{ matrix.os }}
     runs-on: ${{ matrix.os }}
 
 
     # Service containers to run with `runner-job`
     # Service containers to run with `runner-job`
@@ -88,8 +92,8 @@ jobs:
     strategy:
     strategy:
       fail-fast: false
       fail-fast: false
       matrix:
       matrix:
-        # Note: py39, py310 versions chosen due to available arm64 darwin builds.
-        python-version: ['3.9.13', '3.10.11', '3.11.5', '3.12.0', '3.13.0']
+        # Note: py39, py310, py311 versions chosen due to available arm64 darwin builds.
+        python-version: ["3.9.13", "3.10.11", "3.11.9", "3.12.8", "3.13.1"]
     runs-on: macos-latest
     runs-on: macos-latest
     steps:
     steps:
       - uses: actions/checkout@v4
       - uses: actions/checkout@v4
@@ -106,4 +110,4 @@ jobs:
         run: |
         run: |
           export PYTHONUNBUFFERED=1
           export PYTHONUNBUFFERED=1
           poetry run uv pip install "pydantic~=1.10"
           poetry run uv pip install "pydantic~=1.10"
-          poetry run pytest tests/units --cov --no-cov-on-fail --cov-report=
+          poetry run pytest tests/units --cov --no-cov-on-fail --cov-report=

+ 1 - 1
benchmarks/benchmark_package_size.py

@@ -21,7 +21,7 @@ def get_package_size(venv_path: Path, os_name):
         ValueError: when venv does not exist or python version is None.
         ValueError: when venv does not exist or python version is None.
     """
     """
     python_version = get_python_version(venv_path, os_name)
     python_version = get_python_version(venv_path, os_name)
-    print("Python version:", python_version)
+    print("Python version:", python_version)  # noqa: T201
     if python_version is None:
     if python_version is None:
         raise ValueError("Error: Failed to determine Python version.")
         raise ValueError("Error: Failed to determine Python version.")
 
 

+ 4 - 5
pyproject.toml

@@ -1,6 +1,6 @@
 [tool.poetry]
 [tool.poetry]
 name = "reflex"
 name = "reflex"
-version = "0.6.8dev1"
+version = "0.7.0dev1"
 description = "Web apps in pure Python."
 description = "Web apps in pure Python."
 license = "Apache-2.0"
 license = "Apache-2.0"
 authors = [
 authors = [
@@ -16,7 +16,6 @@ repository = "https://github.com/reflex-dev/reflex"
 documentation = "https://reflex.dev/docs/getting-started/introduction"
 documentation = "https://reflex.dev/docs/getting-started/introduction"
 keywords = ["web", "framework"]
 keywords = ["web", "framework"]
 classifiers = ["Development Status :: 4 - Beta"]
 classifiers = ["Development Status :: 4 - Beta"]
-packages = [{ include = "reflex" }]
 
 
 [tool.poetry.dependencies]
 [tool.poetry.dependencies]
 python = "^3.9"
 python = "^3.9"
@@ -88,13 +87,13 @@ reportIncompatibleMethodOverride = false
 target-version = "py39"
 target-version = "py39"
 output-format = "concise"
 output-format = "concise"
 lint.isort.split-on-trailing-comma = false
 lint.isort.split-on-trailing-comma = false
-lint.select = ["B", "C4", "D", "E", "ERA", "F", "FURB", "I", "PERF", "PTH", "RUF", "SIM", "W"]
+lint.select = ["B", "C4", "D", "E", "ERA", "F", "FURB", "I", "PERF", "PTH", "RUF", "SIM", "T", "W"]
 lint.ignore = ["B008", "D205", "E501", "F403", "SIM115", "RUF006", "RUF012"]
 lint.ignore = ["B008", "D205", "E501", "F403", "SIM115", "RUF006", "RUF012"]
 lint.pydocstyle.convention = "google"
 lint.pydocstyle.convention = "google"
 
 
 [tool.ruff.lint.per-file-ignores]
 [tool.ruff.lint.per-file-ignores]
 "__init__.py" = ["F401"]
 "__init__.py" = ["F401"]
-"tests/*.py" = ["D100", "D103", "D104", "B018", "PERF"]
+"tests/*.py" = ["D100", "D103", "D104", "B018", "PERF", "T"]
 "reflex/.templates/*.py" = ["D100", "D103", "D104"]
 "reflex/.templates/*.py" = ["D100", "D103", "D104"]
 "*.pyi" = ["D301", "D415", "D417", "D418", "E742"]
 "*.pyi" = ["D301", "D415", "D417", "D418", "E742"]
 "*/blank.py" = ["I001"]
 "*/blank.py" = ["I001"]
@@ -105,4 +104,4 @@ asyncio_mode = "auto"
 
 
 [tool.codespell]
 [tool.codespell]
 skip = "docs/*,*.html,examples/*, *.pyi"
 skip = "docs/*,*.html,examples/*, *.pyi"
-ignore-words-list = "te, TreeE"
+ignore-words-list = "te, TreeE"

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

@@ -1,4 +1,5 @@
 {% extends "web/pages/base_page.js.jinja2" %}
 {% extends "web/pages/base_page.js.jinja2" %}
+{% from "web/pages/macros.js.jinja2" import renderHooks %}
 
 
 {% block early_imports %}
 {% block early_imports %}
 import '$/styles/styles.css'
 import '$/styles/styles.css'
@@ -18,10 +19,7 @@ import * as {{library_alias}} from "{{library_path}}";
 
 
 {% block export %}
 {% block export %}
 function AppWrap({children}) {
 function AppWrap({children}) {
-
-  {% for hook in hooks %}
-  {{ hook }}
-  {% endfor %}
+  {{ renderHooks(hooks) }}
 
 
   return (
   return (
     {{utils.render(render, indent_width=0)}}
     {{utils.render(render, indent_width=0)}}

+ 3 - 4
reflex/.templates/jinja/web/pages/custom_component.js.jinja2

@@ -1,5 +1,5 @@
 {% extends "web/pages/base_page.js.jinja2" %}
 {% extends "web/pages/base_page.js.jinja2" %}
-
+{% from "web/pages/macros.js.jinja2" import renderHooks %}
 {% block export %}
 {% block export %}
 {% for component in components %}
 {% for component in components %}
 
 
@@ -8,9 +8,8 @@
 {% endfor %}
 {% endfor %}
 
 
 export const {{component.name}} = memo(({ {{-component.props|join(", ")-}} }) => {
 export const {{component.name}} = memo(({ {{-component.props|join(", ")-}} }) => {
-    {% for hook in component.hooks %}
-    {{ hook }}
-    {% endfor %}
+    {{ renderHooks(component.hooks) }}
+
     return(
     return(
         {{utils.render(component.render)}}
         {{utils.render(component.render)}}
       )
       )

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

@@ -1,4 +1,5 @@
 {% extends "web/pages/base_page.js.jinja2" %}
 {% extends "web/pages/base_page.js.jinja2" %}
+{% from "web/pages/macros.js.jinja2" import renderHooks %}
 
 
 {% block declaration %}
 {% block declaration %}
 {% for custom_code in custom_codes %}
 {% for custom_code in custom_codes %}
@@ -8,9 +9,7 @@
 
 
 {% block export %}
 {% block export %}
 export default function Component() {
 export default function Component() {
-  {% for hook in hooks %}
-  {{ hook }}
-  {% endfor %}
+    {{ renderHooks(hooks)}}
 
 
   return (
   return (
     {{utils.render(render, indent_width=0)}}
     {{utils.render(render, indent_width=0)}}

+ 38 - 0
reflex/.templates/jinja/web/pages/macros.js.jinja2

@@ -0,0 +1,38 @@
+{% macro renderHooks(hooks) %}
+  {% set sorted_hooks = sort_hooks(hooks) %}
+
+  {# Render the grouped hooks #}
+   {% for hook, _ in sorted_hooks[const.hook_position.INTERNAL] %}
+  {{ hook }}
+  {% endfor %}
+
+  {% for hook, _ in sorted_hooks[const.hook_position.PRE_TRIGGER] %}
+  {{ hook }}
+  {% endfor %}
+
+  {% for hook, _ in sorted_hooks[const.hook_position.POST_TRIGGER] %}
+  {{ hook }}
+  {% endfor %}
+{% endmacro %}
+
+{% macro renderHooksWithMemo(hooks, memo)%}
+  {% set sorted_hooks = sort_hooks(hooks) %}
+
+  {# Render the grouped hooks #}
+  {% for hook, _ in sorted_hooks[const.hook_position.INTERNAL] %}
+  {{ hook }}
+  {% endfor %}
+
+  {% for hook, _ in sorted_hooks[const.hook_position.PRE_TRIGGER] %}
+  {{ hook }}
+  {% endfor %}
+
+  {% for hook in memo %}
+  {{ hook }}
+  {% endfor %}
+
+  {% for hook, _ in sorted_hooks[const.hook_position.POST_TRIGGER] %}
+  {{ hook }}
+  {% endfor %}
+
+{% endmacro %}

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

@@ -1,22 +1,10 @@
 {% import 'web/pages/utils.js.jinja2' as utils %}
 {% import 'web/pages/utils.js.jinja2' as utils %}
+{% from 'web/pages/macros.js.jinja2' import renderHooksWithMemo %}
+{% set all_hooks = component._get_all_hooks() %}
 
 
 export function {{tag_name}} () {
 export function {{tag_name}} () {
-  {% for hook in component._get_all_hooks_internal() %}
-  {{ hook }}
-  {% endfor %}
-
-  {% for hook, data in component._get_all_hooks().items() if not data.position or data.position == const.hook_position.PRE_TRIGGER %}
-  {{ hook }}
-  {% endfor %}
-
-  {% for hook in memo_trigger_hooks %}
-  {{ hook }}
-  {% endfor %}
-
-  {% for hook, data in component._get_all_hooks().items() if data.position and data.position == const.hook_position.POST_TRIGGER %}
-  {{ hook }}
-  {% endfor %}
-
+  {{ renderHooksWithMemo(all_hooks, memo_trigger_hooks) }}
+  
   return (
   return (
     {{utils.render(component.render(), indent_width=0)}}
     {{utils.render(component.render(), indent_width=0)}}
   )
   )

+ 9 - 4
reflex/.templates/web/utils/state.js

@@ -208,11 +208,16 @@ export const applyEvent = async (event, socket) => {
   if (event.name == "_download") {
   if (event.name == "_download") {
     const a = document.createElement("a");
     const a = document.createElement("a");
     a.hidden = true;
     a.hidden = true;
+    a.href = event.payload.url;
     // Special case when linking to uploaded files
     // Special case when linking to uploaded files
-    a.href = event.payload.url.replace(
-      "${getBackendURL(env.UPLOAD)}",
-      getBackendURL(env.UPLOAD)
-    );
+    if (a.href.includes("getBackendURL(env.UPLOAD)")) {
+      a.href = eval?.(
+        event.payload.url.replace(
+          "getBackendURL(env.UPLOAD)",
+          `"${getBackendURL(env.UPLOAD)}"`
+        )
+      );
+    }
     a.download = event.payload.filename;
     a.download = event.payload.filename;
     a.click();
     a.click();
     a.remove();
     a.remove();

+ 2 - 3
reflex/app.py

@@ -68,6 +68,7 @@ from reflex.components.core.upload import Upload, get_upload_dir
 from reflex.components.radix import themes
 from reflex.components.radix import themes
 from reflex.config import environment, get_config
 from reflex.config import environment, get_config
 from reflex.event import (
 from reflex.event import (
+    _EVENT_FIELDS,
     BASE_STATE,
     BASE_STATE,
     Event,
     Event,
     EventHandler,
     EventHandler,
@@ -1565,9 +1566,7 @@ class EventNamespace(AsyncNamespace):
         """
         """
         fields = data
         fields = data
         # Get the event.
         # Get the event.
-        event = Event(
-            **{k: v for k, v in fields.items() if k not in ("handler", "event_actions")}
-        )
+        event = Event(**{k: v for k, v in fields.items() if k in _EVENT_FIELDS})
 
 
         self.token_to_sid[event.token] = sid
         self.token_to_sid[event.token] = sid
         self.sid_to_token[sid] = event.token
         self.sid_to_token[sid] = event.token

+ 2 - 2
reflex/compiler/compiler.py

@@ -75,7 +75,7 @@ def _compile_app(app_root: Component) -> str:
     return templates.APP_ROOT.render(
     return templates.APP_ROOT.render(
         imports=utils.compile_imports(app_root._get_all_imports()),
         imports=utils.compile_imports(app_root._get_all_imports()),
         custom_codes=app_root._get_all_custom_code(),
         custom_codes=app_root._get_all_custom_code(),
-        hooks={**app_root._get_all_hooks_internal(), **app_root._get_all_hooks()},
+        hooks=app_root._get_all_hooks(),
         window_libraries=window_libraries,
         window_libraries=window_libraries,
         render=app_root.render(),
         render=app_root.render(),
     )
     )
@@ -149,7 +149,7 @@ def _compile_page(
         imports=imports,
         imports=imports,
         dynamic_imports=component._get_all_dynamic_imports(),
         dynamic_imports=component._get_all_dynamic_imports(),
         custom_codes=component._get_all_custom_code(),
         custom_codes=component._get_all_custom_code(),
-        hooks={**component._get_all_hooks_internal(), **component._get_all_hooks()},
+        hooks=component._get_all_hooks(),
         render=component.render(),
         render=component.render(),
         **kwargs,
         **kwargs,
     )
     )

+ 41 - 0
reflex/compiler/templates.py

@@ -1,9 +1,46 @@
 """Templates to use in the reflex compiler."""
 """Templates to use in the reflex compiler."""
 
 
+from __future__ import annotations
+
 from jinja2 import Environment, FileSystemLoader, Template
 from jinja2 import Environment, FileSystemLoader, Template
 
 
 from reflex import constants
 from reflex import constants
+from reflex.constants import Hooks
 from reflex.utils.format import format_state_name, json_dumps
 from reflex.utils.format import format_state_name, json_dumps
+from reflex.vars.base import VarData
+
+
+def _sort_hooks(hooks: dict[str, VarData | None]):
+    """Sort the hooks by their position.
+
+    Args:
+        hooks: The hooks to sort.
+
+    Returns:
+        The sorted hooks.
+    """
+    sorted_hooks = {
+        Hooks.HookPosition.INTERNAL: [],
+        Hooks.HookPosition.PRE_TRIGGER: [],
+        Hooks.HookPosition.POST_TRIGGER: [],
+    }
+
+    for hook, data in hooks.items():
+        if data and data.position and data.position == Hooks.HookPosition.INTERNAL:
+            sorted_hooks[Hooks.HookPosition.INTERNAL].append((hook, data))
+        elif not data or (
+            not data.position
+            or data.position == constants.Hooks.HookPosition.PRE_TRIGGER
+        ):
+            sorted_hooks[Hooks.HookPosition.PRE_TRIGGER].append((hook, data))
+        elif (
+            data
+            and data.position
+            and data.position == constants.Hooks.HookPosition.POST_TRIGGER
+        ):
+            sorted_hooks[Hooks.HookPosition.POST_TRIGGER].append((hook, data))
+
+    return sorted_hooks
 
 
 
 
 class ReflexJinjaEnvironment(Environment):
 class ReflexJinjaEnvironment(Environment):
@@ -47,6 +84,7 @@ class ReflexJinjaEnvironment(Environment):
             "frontend_exception_state": constants.CompileVars.FRONTEND_EXCEPTION_STATE_FULL,
             "frontend_exception_state": constants.CompileVars.FRONTEND_EXCEPTION_STATE_FULL,
             "hook_position": constants.Hooks.HookPosition,
             "hook_position": constants.Hooks.HookPosition,
         }
         }
+        self.globals["sort_hooks"] = _sort_hooks
 
 
 
 
 def get_template(name: str) -> Template:
 def get_template(name: str) -> Template:
@@ -103,6 +141,9 @@ STYLE = get_template("web/styles/styles.css.jinja2")
 # Code that generate the package json file
 # Code that generate the package json file
 PACKAGE_JSON = get_template("web/package.json.jinja2")
 PACKAGE_JSON = get_template("web/package.json.jinja2")
 
 
+# Template containing some macros used in the web pages.
+MACROS = get_template("web/pages/macros.js.jinja2")
+
 # Code that generate the pyproject.toml file for custom components.
 # Code that generate the pyproject.toml file for custom components.
 CUSTOM_COMPONENTS_PYPROJECT_TOML = get_template(
 CUSTOM_COMPONENTS_PYPROJECT_TOML = get_template(
     "custom_components/pyproject.toml.jinja2"
     "custom_components/pyproject.toml.jinja2"

+ 1 - 1
reflex/compiler/utils.py

@@ -290,7 +290,7 @@ def compile_custom_component(
             "name": component.tag,
             "name": component.tag,
             "props": props,
             "props": props,
             "render": render.render(),
             "render": render.render(),
-            "hooks": {**render._get_all_hooks_internal(), **render._get_all_hooks()},
+            "hooks": render._get_all_hooks(),
             "custom_code": render._get_all_custom_code(),
             "custom_code": render._get_all_custom_code(),
         },
         },
         imports,
         imports,

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

@@ -9,6 +9,7 @@ from reflex.components.tags import Tag
 from reflex.components.tags.tagless import Tagless
 from reflex.components.tags.tagless import Tagless
 from reflex.utils.imports import ParsedImportDict
 from reflex.utils.imports import ParsedImportDict
 from reflex.vars import BooleanVar, ObjectVar, Var
 from reflex.vars import BooleanVar, ObjectVar, Var
+from reflex.vars.base import VarData
 
 
 
 
 class Bare(Component):
 class Bare(Component):
@@ -32,7 +33,7 @@ class Bare(Component):
             contents = str(contents) if contents is not None else ""
             contents = str(contents) if contents is not None else ""
         return cls(contents=contents)  # type: ignore
         return cls(contents=contents)  # type: ignore
 
 
-    def _get_all_hooks_internal(self) -> dict[str, None]:
+    def _get_all_hooks_internal(self) -> dict[str, VarData | None]:
         """Include the hooks for the component.
         """Include the hooks for the component.
 
 
         Returns:
         Returns:
@@ -46,7 +47,7 @@ class Bare(Component):
                     hooks |= component._get_all_hooks_internal()
                     hooks |= component._get_all_hooks_internal()
         return hooks
         return hooks
 
 
-    def _get_all_hooks(self) -> dict[str, None]:
+    def _get_all_hooks(self) -> dict[str, VarData | None]:
         """Include the hooks for the component.
         """Include the hooks for the component.
 
 
         Returns:
         Returns:
@@ -122,11 +123,14 @@ class Bare(Component):
             return Tagless(contents=f"{{{self.contents!s}}}")
             return Tagless(contents=f"{{{self.contents!s}}}")
         return Tagless(contents=str(self.contents))
         return Tagless(contents=str(self.contents))
 
 
-    def _get_vars(self, include_children: bool = False) -> Iterator[Var]:
+    def _get_vars(
+        self, include_children: bool = False, ignore_ids: set[int] | None = None
+    ) -> Iterator[Var]:
         """Walk all Vars used in this component.
         """Walk all Vars used in this component.
 
 
         Args:
         Args:
             include_children: Whether to include Vars from children.
             include_children: Whether to include Vars from children.
+            ignore_ids: The ids to ignore.
 
 
         Yields:
         Yields:
             The contents if it is a Var, otherwise nothing.
             The contents if it is a Var, otherwise nothing.

+ 78 - 116
reflex/components/component.py

@@ -23,6 +23,8 @@ from typing import (
     Union,
     Union,
 )
 )
 
 
+from typing_extensions import deprecated
+
 import reflex.state
 import reflex.state
 from reflex.base import Base
 from reflex.base import Base
 from reflex.compiler.templates import STATEFUL_COMPONENT
 from reflex.compiler.templates import STATEFUL_COMPONENT
@@ -43,17 +45,13 @@ from reflex.constants.state import FRONTEND_EVENT_STATE
 from reflex.event import (
 from reflex.event import (
     EventCallback,
     EventCallback,
     EventChain,
     EventChain,
-    EventChainVar,
     EventHandler,
     EventHandler,
     EventSpec,
     EventSpec,
     EventVar,
     EventVar,
-    call_event_fn,
-    call_event_handler,
-    get_handler_args,
     no_args_event_spec,
     no_args_event_spec,
 )
 )
 from reflex.style import Style, format_as_emotion
 from reflex.style import Style, format_as_emotion
-from reflex.utils import format, imports, types
+from reflex.utils import console, format, imports, types
 from reflex.utils.imports import (
 from reflex.utils.imports import (
     ImmutableParsedImportDict,
     ImmutableParsedImportDict,
     ImportDict,
     ImportDict,
@@ -104,7 +102,7 @@ class BaseComponent(Base, ABC):
         """
         """
 
 
     @abstractmethod
     @abstractmethod
-    def _get_all_hooks_internal(self) -> dict[str, None]:
+    def _get_all_hooks_internal(self) -> dict[str, VarData | None]:
         """Get the reflex internal hooks for the component and its children.
         """Get the reflex internal hooks for the component and its children.
 
 
         Returns:
         Returns:
@@ -112,7 +110,7 @@ class BaseComponent(Base, ABC):
         """
         """
 
 
     @abstractmethod
     @abstractmethod
-    def _get_all_hooks(self) -> dict[str, None]:
+    def _get_all_hooks(self) -> dict[str, VarData | None]:
         """Get the React hooks for this component.
         """Get the React hooks for this component.
 
 
         Returns:
         Returns:
@@ -493,8 +491,7 @@ class Component(BaseComponent, ABC):
                     )
                     )
             # Check if the key is an event trigger.
             # Check if the key is an event trigger.
             if key in component_specific_triggers:
             if key in component_specific_triggers:
-                # Temporarily disable full control for event triggers.
-                kwargs["event_triggers"][key] = self._create_event_chain(
+                kwargs["event_triggers"][key] = EventChain.create(
                     value=value,  # type: ignore
                     value=value,  # type: ignore
                     args_spec=component_specific_triggers[key],
                     args_spec=component_specific_triggers[key],
                     key=key,
                     key=key,
@@ -548,6 +545,7 @@ class Component(BaseComponent, ABC):
         # Construct the component.
         # Construct the component.
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
 
 
+    @deprecated("Use rx.EventChain.create instead.")
     def _create_event_chain(
     def _create_event_chain(
         self,
         self,
         args_spec: types.ArgsSpec | Sequence[types.ArgsSpec],
         args_spec: types.ArgsSpec | Sequence[types.ArgsSpec],
@@ -569,82 +567,18 @@ class Component(BaseComponent, ABC):
 
 
         Returns:
         Returns:
             The event chain.
             The event chain.
-
-        Raises:
-            ValueError: If the value is not a valid event chain.
-        """
-        # If it's an event chain var, return it.
-        if isinstance(value, Var):
-            if isinstance(value, EventChainVar):
-                return value
-            elif isinstance(value, EventVar):
-                value = [value]
-            elif issubclass(value._var_type, (EventChain, EventSpec)):
-                return self._create_event_chain(args_spec, value.guess_type(), key=key)
-            else:
-                raise ValueError(
-                    f"Invalid event chain: {value!s} of type {value._var_type}"
-                )
-        elif isinstance(value, EventChain):
-            # Trust that the caller knows what they're doing passing an EventChain directly
-            return value
-
-        # If the input is a single event handler, wrap it in a list.
-        if isinstance(value, (EventHandler, EventSpec)):
-            value = [value]
-
-        # If the input is a list of event handlers, create an event chain.
-        if isinstance(value, List):
-            events: List[Union[EventSpec, EventVar]] = []
-            for v in value:
-                if isinstance(v, (EventHandler, EventSpec)):
-                    # Call the event handler to get the event.
-                    events.append(call_event_handler(v, args_spec, key=key))
-                elif isinstance(v, Callable):
-                    # Call the lambda to get the event chain.
-                    result = call_event_fn(v, args_spec, key=key)
-                    if isinstance(result, Var):
-                        raise ValueError(
-                            f"Invalid event chain: {v}. Cannot use a Var-returning "
-                            "lambda inside an EventChain list."
-                        )
-                    events.extend(result)
-                elif isinstance(v, EventVar):
-                    events.append(v)
-                else:
-                    raise ValueError(f"Invalid event: {v}")
-
-        # If the input is a callable, create an event chain.
-        elif isinstance(value, Callable):
-            result = call_event_fn(value, args_spec, key=key)
-            if isinstance(result, Var):
-                # Recursively call this function if the lambda returned an EventChain Var.
-                return self._create_event_chain(args_spec, result, key=key)
-            events = [*result]
-
-        # Otherwise, raise an error.
-        else:
-            raise ValueError(f"Invalid event chain: {value}")
-
-        # Add args to the event specs if necessary.
-        events = [
-            (e.with_args(get_handler_args(e)) if isinstance(e, EventSpec) else e)
-            for e in events
-        ]
-
-        # Return the event chain.
-        if isinstance(args_spec, Var):
-            return EventChain(
-                events=events,
-                args_spec=None,
-                event_actions={},
-            )
-        else:
-            return EventChain(
-                events=events,
-                args_spec=args_spec,
-                event_actions={},
-            )
+        """
+        console.deprecate(
+            "Component._create_event_chain",
+            "Use rx.EventChain.create instead.",
+            deprecation_version="0.6.8",
+            removal_version="0.7.0",
+        )
+        return EventChain.create(
+            value=value,  # type: ignore
+            args_spec=args_spec,
+            key=key,
+        )
 
 
     def get_event_triggers(
     def get_event_triggers(
         self,
         self,
@@ -1086,18 +1020,22 @@ class Component(BaseComponent, ABC):
                         event_args.append(spec)
                         event_args.append(spec)
                 yield event_trigger, event_args
                 yield event_trigger, event_args
 
 
-    def _get_vars(self, include_children: bool = False) -> list[Var]:
+    def _get_vars(
+        self, include_children: bool = False, ignore_ids: set[int] | None = None
+    ) -> Iterator[Var]:
         """Walk all Vars used in this component.
         """Walk all Vars used in this component.
 
 
         Args:
         Args:
             include_children: Whether to include Vars from children.
             include_children: Whether to include Vars from children.
+            ignore_ids: The ids to ignore.
 
 
-        Returns:
+        Yields:
             Each var referenced by the component (props, styles, event handlers).
             Each var referenced by the component (props, styles, event handlers).
         """
         """
-        vars = getattr(self, "__vars", None)
+        ignore_ids = ignore_ids or set()
+        vars: List[Var] | None = getattr(self, "__vars", None)
         if vars is not None:
         if vars is not None:
-            return vars
+            yield from vars
         vars = self.__vars = []
         vars = self.__vars = []
         # Get Vars associated with event trigger arguments.
         # Get Vars associated with event trigger arguments.
         for _, event_vars in self._get_vars_from_event_triggers(self.event_triggers):
         for _, event_vars in self._get_vars_from_event_triggers(self.event_triggers):
@@ -1141,12 +1079,15 @@ class Component(BaseComponent, ABC):
         # Get Vars associated with children.
         # Get Vars associated with children.
         if include_children:
         if include_children:
             for child in self.children:
             for child in self.children:
-                if not isinstance(child, Component):
+                if not isinstance(child, Component) or id(child) in ignore_ids:
                     continue
                     continue
-                child_vars = child._get_vars(include_children=include_children)
+                ignore_ids.add(id(child))
+                child_vars = child._get_vars(
+                    include_children=include_children, ignore_ids=ignore_ids
+                )
                 vars.extend(child_vars)
                 vars.extend(child_vars)
 
 
-        return vars
+        yield from vars
 
 
     def _event_trigger_values_use_state(self) -> bool:
     def _event_trigger_values_use_state(self) -> bool:
         """Check if the values of a component's event trigger use state.
         """Check if the values of a component's event trigger use state.
@@ -1338,7 +1279,7 @@ class Component(BaseComponent, ABC):
         """
         """
         _imports = {}
         _imports = {}
 
 
-        if self._get_ref_hook():
+        if self._get_ref_hook() is not None:
             # Handle hooks needed for attaching react refs to DOM nodes.
             # Handle hooks needed for attaching react refs to DOM nodes.
             _imports.setdefault("react", set()).add(ImportVar(tag="useRef"))
             _imports.setdefault("react", set()).add(ImportVar(tag="useRef"))
             _imports.setdefault(f"$/{Dirs.STATE_PATH}", set()).add(
             _imports.setdefault(f"$/{Dirs.STATE_PATH}", set()).add(
@@ -1454,7 +1395,7 @@ class Component(BaseComponent, ABC):
                     }}
                     }}
                 }}, []);"""
                 }}, []);"""
 
 
-    def _get_ref_hook(self) -> str | None:
+    def _get_ref_hook(self) -> Var | None:
         """Generate the ref hook for the component.
         """Generate the ref hook for the component.
 
 
         Returns:
         Returns:
@@ -1462,11 +1403,12 @@ class Component(BaseComponent, ABC):
         """
         """
         ref = self.get_ref()
         ref = self.get_ref()
         if ref is not None:
         if ref is not None:
-            return (
-                f"const {ref} = useRef(null); {Var(_js_expr=ref)._as_ref()!s} = {ref};"
+            return Var(
+                f"const {ref} = useRef(null); {Var(_js_expr=ref)._as_ref()!s} = {ref};",
+                _var_data=VarData(position=Hooks.HookPosition.INTERNAL),
             )
             )
 
 
-    def _get_vars_hooks(self) -> dict[str, None]:
+    def _get_vars_hooks(self) -> dict[str, VarData | None]:
         """Get the hooks required by vars referenced in this component.
         """Get the hooks required by vars referenced in this component.
 
 
         Returns:
         Returns:
@@ -1479,27 +1421,38 @@ class Component(BaseComponent, ABC):
                 vars_hooks.update(
                 vars_hooks.update(
                     var_data.hooks
                     var_data.hooks
                     if isinstance(var_data.hooks, dict)
                     if isinstance(var_data.hooks, dict)
-                    else {k: None for k in var_data.hooks}
+                    else {
+                        k: VarData(position=Hooks.HookPosition.INTERNAL)
+                        for k in var_data.hooks
+                    }
                 )
                 )
         return vars_hooks
         return vars_hooks
 
 
-    def _get_events_hooks(self) -> dict[str, None]:
+    def _get_events_hooks(self) -> dict[str, VarData | None]:
         """Get the hooks required by events referenced in this component.
         """Get the hooks required by events referenced in this component.
 
 
         Returns:
         Returns:
             The hooks for the events.
             The hooks for the events.
         """
         """
-        return {Hooks.EVENTS: None} if self.event_triggers else {}
+        return (
+            {Hooks.EVENTS: VarData(position=Hooks.HookPosition.INTERNAL)}
+            if self.event_triggers
+            else {}
+        )
 
 
-    def _get_special_hooks(self) -> dict[str, None]:
+    def _get_special_hooks(self) -> dict[str, VarData | None]:
         """Get the hooks required by special actions referenced in this component.
         """Get the hooks required by special actions referenced in this component.
 
 
         Returns:
         Returns:
             The hooks for special actions.
             The hooks for special actions.
         """
         """
-        return {Hooks.AUTOFOCUS: None} if self.autofocus else {}
+        return (
+            {Hooks.AUTOFOCUS: VarData(position=Hooks.HookPosition.INTERNAL)}
+            if self.autofocus
+            else {}
+        )
 
 
-    def _get_hooks_internal(self) -> dict[str, None]:
+    def _get_hooks_internal(self) -> dict[str, VarData | None]:
         """Get the React hooks for this component managed by the framework.
         """Get the React hooks for this component managed by the framework.
 
 
         Downstream components should NOT override this method to avoid breaking
         Downstream components should NOT override this method to avoid breaking
@@ -1510,7 +1463,7 @@ class Component(BaseComponent, ABC):
         """
         """
         return {
         return {
             **{
             **{
-                hook: None
+                str(hook): VarData(position=Hooks.HookPosition.INTERNAL)
                 for hook in [self._get_ref_hook(), self._get_mount_lifecycle_hook()]
                 for hook in [self._get_ref_hook(), self._get_mount_lifecycle_hook()]
                 if hook is not None
                 if hook is not None
             },
             },
@@ -1559,7 +1512,7 @@ class Component(BaseComponent, ABC):
         """
         """
         return
         return
 
 
-    def _get_all_hooks_internal(self) -> dict[str, None]:
+    def _get_all_hooks_internal(self) -> dict[str, VarData | None]:
         """Get the reflex internal hooks for the component and its children.
         """Get the reflex internal hooks for the component and its children.
 
 
         Returns:
         Returns:
@@ -1574,7 +1527,7 @@ class Component(BaseComponent, ABC):
 
 
         return code
         return code
 
 
-    def _get_all_hooks(self) -> dict[str, None]:
+    def _get_all_hooks(self) -> dict[str, VarData | None]:
         """Get the React hooks for this component and its children.
         """Get the React hooks for this component and its children.
 
 
         Returns:
         Returns:
@@ -1582,6 +1535,9 @@ class Component(BaseComponent, ABC):
         """
         """
         code = {}
         code = {}
 
 
+        # Add the internal hooks for this component.
+        code.update(self._get_hooks_internal())
+
         # Add the hook code for this component.
         # Add the hook code for this component.
         hooks = self._get_hooks()
         hooks = self._get_hooks()
         if hooks is not None:
         if hooks is not None:
@@ -1737,7 +1693,7 @@ class CustomComponent(Component):
 
 
             # Handle event chains.
             # Handle event chains.
             if types._issubclass(type_, EventChain):
             if types._issubclass(type_, EventChain):
-                value = self._create_event_chain(
+                value = EventChain.create(
                     value=value,
                     value=value,
                     args_spec=event_triggers_in_component_declaration.get(
                     args_spec=event_triggers_in_component_declaration.get(
                         key, no_args_event_spec
                         key, no_args_event_spec
@@ -1862,19 +1818,25 @@ class CustomComponent(Component):
             for name, prop in self.props.items()
             for name, prop in self.props.items()
         ]
         ]
 
 
-    def _get_vars(self, include_children: bool = False) -> list[Var]:
+    def _get_vars(
+        self, include_children: bool = False, ignore_ids: set[int] | None = None
+    ) -> Iterator[Var]:
         """Walk all Vars used in this component.
         """Walk all Vars used in this component.
 
 
         Args:
         Args:
             include_children: Whether to include Vars from children.
             include_children: Whether to include Vars from children.
+            ignore_ids: The ids to ignore.
 
 
-        Returns:
+        Yields:
             Each var referenced by the component (props, styles, event handlers).
             Each var referenced by the component (props, styles, event handlers).
         """
         """
-        return (
-            super()._get_vars(include_children=include_children)
-            + [prop for prop in self.props.values() if isinstance(prop, Var)]
-            + self.get_component(self)._get_vars(include_children=include_children)
+        ignore_ids = ignore_ids or set()
+        yield from super()._get_vars(
+            include_children=include_children, ignore_ids=ignore_ids
+        )
+        yield from filter(lambda prop: isinstance(prop, Var), self.props.values())
+        yield from self.get_component(self)._get_vars(
+            include_children=include_children, ignore_ids=ignore_ids
         )
         )
 
 
     @lru_cache(maxsize=None)  # noqa
     @lru_cache(maxsize=None)  # noqa
@@ -2277,7 +2239,7 @@ class StatefulComponent(BaseComponent):
             )
             )
         return trigger_memo
         return trigger_memo
 
 
-    def _get_all_hooks_internal(self) -> dict[str, None]:
+    def _get_all_hooks_internal(self) -> dict[str, VarData | None]:
         """Get the reflex internal hooks for the component and its children.
         """Get the reflex internal hooks for the component and its children.
 
 
         Returns:
         Returns:
@@ -2285,7 +2247,7 @@ class StatefulComponent(BaseComponent):
         """
         """
         return {}
         return {}
 
 
-    def _get_all_hooks(self) -> dict[str, None]:
+    def _get_all_hooks(self) -> dict[str, VarData | None]:
         """Get the React hooks for this component.
         """Get the React hooks for this component.
 
 
         Returns:
         Returns:
@@ -2403,7 +2365,7 @@ class MemoizationLeaf(Component):
             The memoization leaf
             The memoization leaf
         """
         """
         comp = super().create(*children, **props)
         comp = super().create(*children, **props)
-        if comp._get_all_hooks() or comp._get_all_hooks_internal():
+        if comp._get_all_hooks():
             comp._memoization_mode = cls._memoization_mode.copy(
             comp._memoization_mode = cls._memoization_mode.copy(
                 update={"disposition": MemoizationDisposition.ALWAYS}
                 update={"disposition": MemoizationDisposition.ALWAYS}
             )
             )

+ 13 - 9
reflex/components/datadisplay/code.py

@@ -502,8 +502,8 @@ class CodeBlock(Component, MarkdownComponentMap):
 
 
         theme = self.theme
         theme = self.theme
 
 
-        out.add_props(style=theme).remove_props("theme", "code", "language").add_props(
-            children=self.code, language=_LANGUAGE
+        out.add_props(style=theme).remove_props("theme", "code").add_props(
+            children=self.code,
         )
         )
 
 
         return out
         return out
@@ -512,20 +512,25 @@ class CodeBlock(Component, MarkdownComponentMap):
         return ["can_copy", "copy_button"]
         return ["can_copy", "copy_button"]
 
 
     @classmethod
     @classmethod
-    def _get_language_registration_hook(cls) -> str:
+    def _get_language_registration_hook(cls, language_var: Var = _LANGUAGE) -> str:
         """Get the hook to register the language.
         """Get the hook to register the language.
 
 
+        Args:
+            language_var: The const/literal Var of the language module to import.
+                For markdown, uses the default placeholder _LANGUAGE. For direct use,
+                a LiteralStringVar should be passed via the language prop.
+
         Returns:
         Returns:
             The hook to register the language.
             The hook to register the language.
         """
         """
         return f"""
         return f"""
- if ({_LANGUAGE!s}) {{
+ if ({language_var!s}) {{
     (async () => {{
     (async () => {{
       try {{
       try {{
-        const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${{{_LANGUAGE!s}}}`);
-        SyntaxHighlighter.registerLanguage({_LANGUAGE!s}, module.default);
+        const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${{{language_var!s}}}`);
+        SyntaxHighlighter.registerLanguage({language_var!s}, module.default);
       }} catch (error) {{
       }} catch (error) {{
-        console.error(`Error importing language module for ${{{_LANGUAGE!s}}}:`, error);
+        console.error(`Error importing language module for ${{{language_var!s}}}:`, error);
       }}
       }}
     }})();
     }})();
   }}
   }}
@@ -547,8 +552,7 @@ class CodeBlock(Component, MarkdownComponentMap):
             The hooks for the component.
             The hooks for the component.
         """
         """
         return [
         return [
-            f"const {_LANGUAGE!s} = {self.language!s}",
-            self._get_language_registration_hook(),
+            self._get_language_registration_hook(language_var=self.language),
         ]
         ]
 
 
 
 

+ 7 - 5
reflex/components/el/elements/forms.py

@@ -182,9 +182,7 @@ class Form(BaseHTML):
         props["handle_submit_unique_name"] = ""
         props["handle_submit_unique_name"] = ""
         form = super().create(*children, **props)
         form = super().create(*children, **props)
         form.handle_submit_unique_name = md5(
         form.handle_submit_unique_name = md5(
-            str({**form._get_all_hooks_internal(), **form._get_all_hooks()}).encode(
-                "utf-8"
-            )
+            str(form._get_all_hooks()).encode("utf-8")
         ).hexdigest()
         ).hexdigest()
         return form
         return form
 
 
@@ -252,8 +250,12 @@ class Form(BaseHTML):
                 )
                 )
         return form_refs
         return form_refs
 
 
-    def _get_vars(self, include_children: bool = True) -> Iterator[Var]:
-        yield from super()._get_vars(include_children=include_children)
+    def _get_vars(
+        self, include_children: bool = True, ignore_ids: set[int] | None = None
+    ) -> Iterator[Var]:
+        yield from super()._get_vars(
+            include_children=include_children, ignore_ids=ignore_ids
+        )
         yield from self._get_form_refs().values()
         yield from self._get_form_refs().values()
 
 
     def _exclude_props(self) -> list[str]:
     def _exclude_props(self) -> list[str]:

+ 155 - 20
reflex/components/lucide/icon.py

@@ -8,7 +8,7 @@ from reflex.vars.base import Var
 class LucideIconComponent(Component):
 class LucideIconComponent(Component):
     """Lucide Icon Component."""
     """Lucide Icon Component."""
 
 
-    library = "lucide-react@0.359.0"
+    library = "lucide-react@0.469.0"
 
 
 
 
 class Icon(LucideIconComponent):
 class Icon(LucideIconComponent):
@@ -56,7 +56,12 @@ class Icon(LucideIconComponent):
                 "\nSee full list at https://lucide.dev/icons."
                 "\nSee full list at https://lucide.dev/icons."
             )
             )
 
 
-        props["tag"] = format.to_title_case(format.to_snake_case(props["tag"])) + "Icon"
+        if props["tag"] in LUCIDE_ICON_MAPPING_OVERRIDE:
+            props["tag"] = LUCIDE_ICON_MAPPING_OVERRIDE[props["tag"]]
+        else:
+            props["tag"] = (
+                format.to_title_case(format.to_snake_case(props["tag"])) + "Icon"
+            )
         props["alias"] = f"Lucide{props['tag']}"
         props["alias"] = f"Lucide{props['tag']}"
         props.setdefault("color", "var(--current-color)")
         props.setdefault("color", "var(--current-color)")
         return super().create(*children, **props)
         return super().create(*children, **props)
@@ -106,6 +111,7 @@ LUCIDE_ICON_LIST = [
     "ambulance",
     "ambulance",
     "ampersand",
     "ampersand",
     "ampersands",
     "ampersands",
+    "amphora",
     "anchor",
     "anchor",
     "angry",
     "angry",
     "annoyed",
     "annoyed",
@@ -193,6 +199,7 @@ LUCIDE_ICON_LIST = [
     "baggage_claim",
     "baggage_claim",
     "ban",
     "ban",
     "banana",
     "banana",
+    "bandage",
     "banknote",
     "banknote",
     "bar_chart",
     "bar_chart",
     "bar_chart_2",
     "bar_chart_2",
@@ -230,8 +237,10 @@ LUCIDE_ICON_LIST = [
     "between_horizontal_start",
     "between_horizontal_start",
     "between_vertical_end",
     "between_vertical_end",
     "between_vertical_start",
     "between_vertical_start",
+    "biceps_flexed",
     "bike",
     "bike",
     "binary",
     "binary",
+    "binoculars",
     "biohazard",
     "biohazard",
     "bird",
     "bird",
     "bitcoin",
     "bitcoin",
@@ -278,6 +287,7 @@ LUCIDE_ICON_LIST = [
     "boom_box",
     "boom_box",
     "bot",
     "bot",
     "bot_message_square",
     "bot_message_square",
+    "bot_off",
     "box",
     "box",
     "box_select",
     "box_select",
     "boxes",
     "boxes",
@@ -289,6 +299,7 @@ LUCIDE_ICON_LIST = [
     "brick_wall",
     "brick_wall",
     "briefcase",
     "briefcase",
     "briefcase_business",
     "briefcase_business",
+    "briefcase_conveyor_belt",
     "briefcase_medical",
     "briefcase_medical",
     "bring_to_front",
     "bring_to_front",
     "brush",
     "brush",
@@ -305,9 +316,13 @@ LUCIDE_ICON_LIST = [
     "cake_slice",
     "cake_slice",
     "calculator",
     "calculator",
     "calendar",
     "calendar",
+    "calendar_1",
+    "calendar_arrow_down",
+    "calendar_arrow_up",
     "calendar_check",
     "calendar_check",
     "calendar_check_2",
     "calendar_check_2",
     "calendar_clock",
     "calendar_clock",
+    "calendar_cog",
     "calendar_days",
     "calendar_days",
     "calendar_fold",
     "calendar_fold",
     "calendar_heart",
     "calendar_heart",
@@ -318,6 +333,7 @@ LUCIDE_ICON_LIST = [
     "calendar_plus_2",
     "calendar_plus_2",
     "calendar_range",
     "calendar_range",
     "calendar_search",
     "calendar_search",
+    "calendar_sync",
     "calendar_x",
     "calendar_x",
     "calendar_x_2",
     "calendar_x_2",
     "camera",
     "camera",
@@ -342,6 +358,29 @@ LUCIDE_ICON_LIST = [
     "castle",
     "castle",
     "cat",
     "cat",
     "cctv",
     "cctv",
+    "chart_area",
+    "chart_bar",
+    "chart_bar_big",
+    "chart_bar_decreasing",
+    "chart_bar_increasing",
+    "chart_bar_stacked",
+    "chart_candlestick",
+    "chart_column",
+    "chart_column_big",
+    "chart_column_decreasing",
+    "chart_column_increasing",
+    "chart_column_stacked",
+    "chart_gantt",
+    "chart_line",
+    "chart_network",
+    "chart_no_axes_column",
+    "chart_no_axes_column_decreasing",
+    "chart_no_axes_column_increasing",
+    "chart_no_axes_combined",
+    "chart_no_axes_gantt",
+    "chart_pie",
+    "chart_scatter",
+    "chart_spline",
     "check",
     "check",
     "check_check",
     "check_check",
     "chef_hat",
     "chef_hat",
@@ -356,6 +395,7 @@ LUCIDE_ICON_LIST = [
     "chevrons_down_up",
     "chevrons_down_up",
     "chevrons_left",
     "chevrons_left",
     "chevrons_left_right",
     "chevrons_left_right",
+    "chevrons_left_right_ellipsis",
     "chevrons_right",
     "chevrons_right",
     "chevrons_right_left",
     "chevrons_right_left",
     "chevrons_up",
     "chevrons_up",
@@ -374,8 +414,8 @@ LUCIDE_ICON_LIST = [
     "circle_arrow_out_up_right",
     "circle_arrow_out_up_right",
     "circle_arrow_right",
     "circle_arrow_right",
     "circle_arrow_up",
     "circle_arrow_up",
-    "circle_check_big",
     "circle_check",
     "circle_check",
+    "circle_check_big",
     "circle_chevron_down",
     "circle_chevron_down",
     "circle_chevron_left",
     "circle_chevron_left",
     "circle_chevron_right",
     "circle_chevron_right",
@@ -387,13 +427,14 @@ LUCIDE_ICON_LIST = [
     "circle_dot_dashed",
     "circle_dot_dashed",
     "circle_ellipsis",
     "circle_ellipsis",
     "circle_equal",
     "circle_equal",
+    "circle_fading_arrow_up",
     "circle_fading_plus",
     "circle_fading_plus",
     "circle_gauge",
     "circle_gauge",
     "circle_help",
     "circle_help",
     "circle_minus",
     "circle_minus",
     "circle_off",
     "circle_off",
-    "circle_parking_off",
     "circle_parking",
     "circle_parking",
+    "circle_parking_off",
     "circle_pause",
     "circle_pause",
     "circle_percent",
     "circle_percent",
     "circle_play",
     "circle_play",
@@ -432,7 +473,11 @@ LUCIDE_ICON_LIST = [
     "clock_7",
     "clock_7",
     "clock_8",
     "clock_8",
     "clock_9",
     "clock_9",
+    "clock_alert",
+    "clock_arrow_down",
+    "clock_arrow_up",
     "cloud",
     "cloud",
+    "cloud_alert",
     "cloud_cog",
     "cloud_cog",
     "cloud_download",
     "cloud_download",
     "cloud_drizzle",
     "cloud_drizzle",
@@ -503,6 +548,7 @@ LUCIDE_ICON_LIST = [
     "cup_soda",
     "cup_soda",
     "currency",
     "currency",
     "cylinder",
     "cylinder",
+    "dam",
     "database",
     "database",
     "database_backup",
     "database_backup",
     "database_zap",
     "database_zap",
@@ -510,7 +556,9 @@ LUCIDE_ICON_LIST = [
     "dessert",
     "dessert",
     "diameter",
     "diameter",
     "diamond",
     "diamond",
+    "diamond_minus",
     "diamond_percent",
     "diamond_percent",
+    "diamond_plus",
     "dice_1",
     "dice_1",
     "dice_2",
     "dice_2",
     "dice_3",
     "dice_3",
@@ -539,6 +587,7 @@ LUCIDE_ICON_LIST = [
     "dribbble",
     "dribbble",
     "drill",
     "drill",
     "droplet",
     "droplet",
+    "droplet_off",
     "droplets",
     "droplets",
     "drum",
     "drum",
     "drumstick",
     "drumstick",
@@ -554,12 +603,15 @@ LUCIDE_ICON_LIST = [
     "ellipsis",
     "ellipsis",
     "ellipsis_vertical",
     "ellipsis_vertical",
     "equal",
     "equal",
+    "equal_approximately",
     "equal_not",
     "equal_not",
     "eraser",
     "eraser",
+    "ethernet_port",
     "euro",
     "euro",
     "expand",
     "expand",
     "external_link",
     "external_link",
     "eye",
     "eye",
+    "eye_closed",
     "eye_off",
     "eye_off",
     "facebook",
     "facebook",
     "factory",
     "factory",
@@ -579,6 +631,10 @@ LUCIDE_ICON_LIST = [
     "file_bar_chart",
     "file_bar_chart",
     "file_bar_chart_2",
     "file_bar_chart_2",
     "file_box",
     "file_box",
+    "file_chart_column",
+    "file_chart_column_increasing",
+    "file_chart_line",
+    "file_chart_pie",
     "file_check",
     "file_check",
     "file_check_2",
     "file_check_2",
     "file_clock",
     "file_clock",
@@ -620,6 +676,7 @@ LUCIDE_ICON_LIST = [
     "file_type",
     "file_type",
     "file_type_2",
     "file_type_2",
     "file_up",
     "file_up",
+    "file_user",
     "file_video",
     "file_video",
     "file_video_2",
     "file_video_2",
     "file_volume",
     "file_volume",
@@ -661,6 +718,7 @@ LUCIDE_ICON_LIST = [
     "folder_check",
     "folder_check",
     "folder_clock",
     "folder_clock",
     "folder_closed",
     "folder_closed",
+    "folder_code",
     "folder_cog",
     "folder_cog",
     "folder_dot",
     "folder_dot",
     "folder_down",
     "folder_down",
@@ -733,7 +791,12 @@ LUCIDE_ICON_LIST = [
     "graduation_cap",
     "graduation_cap",
     "grape",
     "grape",
     "grid_2x2",
     "grid_2x2",
+    "grid_2x_2",
+    "grid_2x_2_check",
+    "grid_2x_2_plus",
+    "grid_2x_2_x",
     "grid_3x3",
     "grid_3x3",
+    "grid_3x_3",
     "grip",
     "grip",
     "grip_horizontal",
     "grip_horizontal",
     "grip_vertical",
     "grip_vertical",
@@ -762,6 +825,7 @@ LUCIDE_ICON_LIST = [
     "heading_4",
     "heading_4",
     "heading_5",
     "heading_5",
     "heading_6",
     "heading_6",
+    "headphone_off",
     "headphones",
     "headphones",
     "headset",
     "headset",
     "heart",
     "heart",
@@ -779,14 +843,20 @@ LUCIDE_ICON_LIST = [
     "hospital",
     "hospital",
     "hotel",
     "hotel",
     "hourglass",
     "hourglass",
+    "house",
+    "house_plug",
+    "house_plus",
     "ice_cream_bowl",
     "ice_cream_bowl",
     "ice_cream_cone",
     "ice_cream_cone",
+    "id_card",
     "image",
     "image",
     "image_down",
     "image_down",
     "image_minus",
     "image_minus",
     "image_off",
     "image_off",
+    "image_play",
     "image_plus",
     "image_plus",
     "image_up",
     "image_up",
+    "image_upscale",
     "images",
     "images",
     "import",
     "import",
     "inbox",
     "inbox",
@@ -808,6 +878,7 @@ LUCIDE_ICON_LIST = [
     "key_square",
     "key_square",
     "keyboard",
     "keyboard",
     "keyboard_music",
     "keyboard_music",
+    "keyboard_off",
     "lamp",
     "lamp",
     "lamp_ceiling",
     "lamp_ceiling",
     "lamp_desk",
     "lamp_desk",
@@ -817,8 +888,9 @@ LUCIDE_ICON_LIST = [
     "land_plot",
     "land_plot",
     "landmark",
     "landmark",
     "languages",
     "languages",
-    "laptop_minimal",
     "laptop",
     "laptop",
+    "laptop_minimal",
+    "laptop_minimal_check",
     "lasso",
     "lasso",
     "lasso_select",
     "lasso_select",
     "laugh",
     "laugh",
@@ -833,6 +905,8 @@ LUCIDE_ICON_LIST = [
     "layout_template",
     "layout_template",
     "leaf",
     "leaf",
     "leafy_green",
     "leafy_green",
+    "lectern",
+    "letter_text",
     "library",
     "library",
     "library_big",
     "library_big",
     "life_buoy",
     "life_buoy",
@@ -845,10 +919,12 @@ LUCIDE_ICON_LIST = [
     "link_2_off",
     "link_2_off",
     "linkedin",
     "linkedin",
     "list",
     "list",
+    "list_check",
     "list_checks",
     "list_checks",
     "list_collapse",
     "list_collapse",
     "list_end",
     "list_end",
     "list_filter",
     "list_filter",
+    "list_filter_plus",
     "list_minus",
     "list_minus",
     "list_music",
     "list_music",
     "list_ordered",
     "list_ordered",
@@ -861,15 +937,17 @@ LUCIDE_ICON_LIST = [
     "list_x",
     "list_x",
     "loader",
     "loader",
     "loader_circle",
     "loader_circle",
+    "loader_pinwheel",
     "locate",
     "locate",
     "locate_fixed",
     "locate_fixed",
     "locate_off",
     "locate_off",
     "lock",
     "lock",
-    "lock_keyhole_open",
     "lock_keyhole",
     "lock_keyhole",
+    "lock_keyhole_open",
     "lock_open",
     "lock_open",
     "log_in",
     "log_in",
     "log_out",
     "log_out",
+    "logs",
     "lollipop",
     "lollipop",
     "luggage",
     "luggage",
     "magnet",
     "magnet",
@@ -886,7 +964,16 @@ LUCIDE_ICON_LIST = [
     "mails",
     "mails",
     "map",
     "map",
     "map_pin",
     "map_pin",
+    "map_pin_check",
+    "map_pin_check_inside",
+    "map_pin_house",
+    "map_pin_minus",
+    "map_pin_minus_inside",
     "map_pin_off",
     "map_pin_off",
+    "map_pin_plus",
+    "map_pin_plus_inside",
+    "map_pin_x",
+    "map_pin_x_inside",
     "map_pinned",
     "map_pinned",
     "martini",
     "martini",
     "maximize",
     "maximize",
@@ -915,6 +1002,7 @@ LUCIDE_ICON_LIST = [
     "message_square_diff",
     "message_square_diff",
     "message_square_dot",
     "message_square_dot",
     "message_square_heart",
     "message_square_heart",
+    "message_square_lock",
     "message_square_more",
     "message_square_more",
     "message_square_off",
     "message_square_off",
     "message_square_plus",
     "message_square_plus",
@@ -926,8 +1014,9 @@ LUCIDE_ICON_LIST = [
     "message_square_x",
     "message_square_x",
     "messages_square",
     "messages_square",
     "mic",
     "mic",
-    "mic_vocal",
     "mic_off",
     "mic_off",
+    "mic_vocal",
+    "microchip",
     "microscope",
     "microscope",
     "microwave",
     "microwave",
     "milestone",
     "milestone",
@@ -938,6 +1027,7 @@ LUCIDE_ICON_LIST = [
     "minus",
     "minus",
     "monitor",
     "monitor",
     "monitor_check",
     "monitor_check",
+    "monitor_cog",
     "monitor_dot",
     "monitor_dot",
     "monitor_down",
     "monitor_down",
     "monitor_off",
     "monitor_off",
@@ -953,8 +1043,10 @@ LUCIDE_ICON_LIST = [
     "mountain",
     "mountain",
     "mountain_snow",
     "mountain_snow",
     "mouse",
     "mouse",
+    "mouse_off",
     "mouse_pointer",
     "mouse_pointer",
     "mouse_pointer_2",
     "mouse_pointer_2",
+    "mouse_pointer_ban",
     "mouse_pointer_click",
     "mouse_pointer_click",
     "move",
     "move",
     "move_3d",
     "move_3d",
@@ -991,10 +1083,13 @@ LUCIDE_ICON_LIST = [
     "nut_off",
     "nut_off",
     "octagon",
     "octagon",
     "octagon_alert",
     "octagon_alert",
+    "octagon_minus",
     "octagon_pause",
     "octagon_pause",
     "octagon_x",
     "octagon_x",
+    "omega",
     "option",
     "option",
     "orbit",
     "orbit",
+    "origami",
     "package",
     "package",
     "package_2",
     "package_2",
     "package_check",
     "package_check",
@@ -1007,6 +1102,7 @@ LUCIDE_ICON_LIST = [
     "paint_roller",
     "paint_roller",
     "paintbrush",
     "paintbrush",
     "paintbrush_2",
     "paintbrush_2",
+    "paintbrush_vertical",
     "palette",
     "palette",
     "panel_bottom",
     "panel_bottom",
     "panel_bottom_close",
     "panel_bottom_close",
@@ -1036,13 +1132,16 @@ LUCIDE_ICON_LIST = [
     "pc_case",
     "pc_case",
     "pen",
     "pen",
     "pen_line",
     "pen_line",
+    "pen_off",
     "pen_tool",
     "pen_tool",
     "pencil",
     "pencil",
     "pencil_line",
     "pencil_line",
+    "pencil_off",
     "pencil_ruler",
     "pencil_ruler",
     "pentagon",
     "pentagon",
     "percent",
     "percent",
     "person_standing",
     "person_standing",
+    "philippine_peso",
     "phone",
     "phone",
     "phone_call",
     "phone_call",
     "phone_forwarded",
     "phone_forwarded",
@@ -1058,7 +1157,10 @@ LUCIDE_ICON_LIST = [
     "pie_chart",
     "pie_chart",
     "piggy_bank",
     "piggy_bank",
     "pilcrow",
     "pilcrow",
+    "pilcrow_left",
+    "pilcrow_right",
     "pill",
     "pill",
+    "pill_bottle",
     "pin",
     "pin",
     "pin_off",
     "pin_off",
     "pipette",
     "pipette",
@@ -1084,6 +1186,7 @@ LUCIDE_ICON_LIST = [
     "power_off",
     "power_off",
     "presentation",
     "presentation",
     "printer",
     "printer",
+    "printer_check",
     "projector",
     "projector",
     "proportions",
     "proportions",
     "puzzle",
     "puzzle",
@@ -1158,6 +1261,7 @@ LUCIDE_ICON_LIST = [
     "satellite_dish",
     "satellite_dish",
     "save",
     "save",
     "save_all",
     "save_all",
+    "save_off",
     "scale",
     "scale",
     "scale_3d",
     "scale_3d",
     "scaling",
     "scaling",
@@ -1165,7 +1269,9 @@ LUCIDE_ICON_LIST = [
     "scan_barcode",
     "scan_barcode",
     "scan_eye",
     "scan_eye",
     "scan_face",
     "scan_face",
+    "scan_heart",
     "scan_line",
     "scan_line",
+    "scan_qr_code",
     "scan_search",
     "scan_search",
     "scan_text",
     "scan_text",
     "scatter_chart",
     "scatter_chart",
@@ -1181,6 +1287,7 @@ LUCIDE_ICON_LIST = [
     "search_code",
     "search_code",
     "search_slash",
     "search_slash",
     "search_x",
     "search_x",
+    "section",
     "send",
     "send",
     "send_horizontal",
     "send_horizontal",
     "send_to_back",
     "send_to_back",
@@ -1225,6 +1332,7 @@ LUCIDE_ICON_LIST = [
     "signal_low",
     "signal_low",
     "signal_medium",
     "signal_medium",
     "signal_zero",
     "signal_zero",
+    "signature",
     "signpost",
     "signpost",
     "signpost_big",
     "signpost_big",
     "siren",
     "siren",
@@ -1234,8 +1342,8 @@ LUCIDE_ICON_LIST = [
     "slack",
     "slack",
     "slash",
     "slash",
     "slice",
     "slice",
-    "sliders_vertical",
     "sliders_horizontal",
     "sliders_horizontal",
+    "sliders_vertical",
     "smartphone",
     "smartphone",
     "smartphone_charging",
     "smartphone_charging",
     "smartphone_nfc",
     "smartphone_nfc",
@@ -1259,29 +1367,31 @@ LUCIDE_ICON_LIST = [
     "sprout",
     "sprout",
     "square",
     "square",
     "square_activity",
     "square_activity",
+    "square_arrow_down",
     "square_arrow_down_left",
     "square_arrow_down_left",
     "square_arrow_down_right",
     "square_arrow_down_right",
-    "square_arrow_down",
     "square_arrow_left",
     "square_arrow_left",
     "square_arrow_out_down_left",
     "square_arrow_out_down_left",
     "square_arrow_out_down_right",
     "square_arrow_out_down_right",
     "square_arrow_out_up_left",
     "square_arrow_out_up_left",
     "square_arrow_out_up_right",
     "square_arrow_out_up_right",
     "square_arrow_right",
     "square_arrow_right",
+    "square_arrow_up",
     "square_arrow_up_left",
     "square_arrow_up_left",
     "square_arrow_up_right",
     "square_arrow_up_right",
-    "square_arrow_up",
     "square_asterisk",
     "square_asterisk",
     "square_bottom_dashed_scissors",
     "square_bottom_dashed_scissors",
-    "square_check_big",
+    "square_chart_gantt",
     "square_check",
     "square_check",
+    "square_check_big",
     "square_chevron_down",
     "square_chevron_down",
     "square_chevron_left",
     "square_chevron_left",
     "square_chevron_right",
     "square_chevron_right",
     "square_chevron_up",
     "square_chevron_up",
     "square_code",
     "square_code",
-    "square_dashed_bottom_code",
+    "square_dashed",
     "square_dashed_bottom",
     "square_dashed_bottom",
+    "square_dashed_bottom_code",
     "square_dashed_kanban",
     "square_dashed_kanban",
     "square_dashed_mouse_pointer",
     "square_dashed_mouse_pointer",
     "square_divide",
     "square_divide",
@@ -1295,8 +1405,8 @@ LUCIDE_ICON_LIST = [
     "square_menu",
     "square_menu",
     "square_minus",
     "square_minus",
     "square_mouse_pointer",
     "square_mouse_pointer",
-    "square_parking_off",
     "square_parking",
     "square_parking",
+    "square_parking_off",
     "square_pen",
     "square_pen",
     "square_percent",
     "square_percent",
     "square_pi",
     "square_pi",
@@ -1310,10 +1420,11 @@ LUCIDE_ICON_LIST = [
     "square_slash",
     "square_slash",
     "square_split_horizontal",
     "square_split_horizontal",
     "square_split_vertical",
     "square_split_vertical",
+    "square_square",
     "square_stack",
     "square_stack",
     "square_terminal",
     "square_terminal",
-    "square_user_round",
     "square_user",
     "square_user",
+    "square_user_round",
     "square_x",
     "square_x",
     "squircle",
     "squircle",
     "squirrel",
     "squirrel",
@@ -1350,6 +1461,7 @@ LUCIDE_ICON_LIST = [
     "table_cells_merge",
     "table_cells_merge",
     "table_cells_split",
     "table_cells_split",
     "table_columns_split",
     "table_columns_split",
+    "table_of_contents",
     "table_properties",
     "table_properties",
     "table_rows_split",
     "table_rows_split",
     "tablet",
     "tablet",
@@ -1365,11 +1477,11 @@ LUCIDE_ICON_LIST = [
     "tangent",
     "tangent",
     "target",
     "target",
     "telescope",
     "telescope",
+    "tent",
     "tent_tree",
     "tent_tree",
     "terminal",
     "terminal",
-    "test_tube_diagonal",
     "test_tube",
     "test_tube",
-    "tent",
+    "test_tube_diagonal",
     "test_tubes",
     "test_tubes",
     "text",
     "text",
     "text_cursor",
     "text_cursor",
@@ -1390,11 +1502,14 @@ LUCIDE_ICON_LIST = [
     "ticket_plus",
     "ticket_plus",
     "ticket_slash",
     "ticket_slash",
     "ticket_x",
     "ticket_x",
+    "tickets",
+    "tickets_plane",
     "timer",
     "timer",
     "timer_off",
     "timer_off",
     "timer_reset",
     "timer_reset",
     "toggle_left",
     "toggle_left",
     "toggle_right",
     "toggle_right",
+    "toilet",
     "tornado",
     "tornado",
     "torus",
     "torus",
     "touchpad",
     "touchpad",
@@ -1416,17 +1531,21 @@ LUCIDE_ICON_LIST = [
     "trello",
     "trello",
     "trending_down",
     "trending_down",
     "trending_up",
     "trending_up",
+    "trending_up_down",
     "triangle",
     "triangle",
-    "triangle_right",
     "triangle_alert",
     "triangle_alert",
+    "triangle_right",
     "trophy",
     "trophy",
     "truck",
     "truck",
     "turtle",
     "turtle",
     "tv",
     "tv",
     "tv_2",
     "tv_2",
+    "tv_minimal",
+    "tv_minimal_play",
     "twitch",
     "twitch",
     "twitter",
     "twitter",
     "type",
     "type",
+    "type_outline",
     "umbrella",
     "umbrella",
     "umbrella_off",
     "umbrella_off",
     "underline",
     "underline",
@@ -1437,8 +1556,8 @@ LUCIDE_ICON_LIST = [
     "unfold_vertical",
     "unfold_vertical",
     "ungroup",
     "ungroup",
     "university",
     "university",
-    "unlink_2",
     "unlink",
     "unlink",
+    "unlink_2",
     "unplug",
     "unplug",
     "upload",
     "upload",
     "usb",
     "usb",
@@ -1446,11 +1565,13 @@ LUCIDE_ICON_LIST = [
     "user_check",
     "user_check",
     "user_cog",
     "user_cog",
     "user_minus",
     "user_minus",
+    "user_pen",
     "user_plus",
     "user_plus",
     "user_round",
     "user_round",
     "user_round_check",
     "user_round_check",
     "user_round_cog",
     "user_round_cog",
     "user_round_minus",
     "user_round_minus",
+    "user_round_pen",
     "user_round_plus",
     "user_round_plus",
     "user_round_search",
     "user_round_search",
     "user_round_x",
     "user_round_x",
@@ -1472,14 +1593,16 @@ LUCIDE_ICON_LIST = [
     "videotape",
     "videotape",
     "view",
     "view",
     "voicemail",
     "voicemail",
+    "volleyball",
     "volume",
     "volume",
     "volume_1",
     "volume_1",
     "volume_2",
     "volume_2",
+    "volume_off",
     "volume_x",
     "volume_x",
     "vote",
     "vote",
     "wallet",
     "wallet",
-    "wallet_minimal",
     "wallet_cards",
     "wallet_cards",
+    "wallet_minimal",
     "wallpaper",
     "wallpaper",
     "wand",
     "wand",
     "wand_sparkles",
     "wand_sparkles",
@@ -1487,17 +1610,22 @@ LUCIDE_ICON_LIST = [
     "washing_machine",
     "washing_machine",
     "watch",
     "watch",
     "waves",
     "waves",
+    "waves_ladder",
     "waypoints",
     "waypoints",
     "webcam",
     "webcam",
-    "webhook_off",
     "webhook",
     "webhook",
+    "webhook_off",
     "weight",
     "weight",
     "wheat",
     "wheat",
     "wheat_off",
     "wheat_off",
     "whole_word",
     "whole_word",
     "wifi",
     "wifi",
+    "wifi_high",
+    "wifi_low",
     "wifi_off",
     "wifi_off",
+    "wifi_zero",
     "wind",
     "wind",
+    "wind_arrow_down",
     "wine",
     "wine",
     "wine_off",
     "wine_off",
     "workflow",
     "workflow",
@@ -1511,3 +1639,10 @@ LUCIDE_ICON_LIST = [
     "zoom_in",
     "zoom_in",
     "zoom_out",
     "zoom_out",
 ]
 ]
+
+# The default transformation of some icon names doesn't match how the
+# icons are exported from Lucide. Manual overrides can go here.
+LUCIDE_ICON_MAPPING_OVERRIDE = {
+    "grid_2x_2_check": "Grid2x2Check",
+    "grid_2x_2_x": "Grid2x2X",
+}

+ 145 - 18
reflex/components/lucide/icon.pyi

@@ -154,6 +154,7 @@ LUCIDE_ICON_LIST = [
     "ambulance",
     "ambulance",
     "ampersand",
     "ampersand",
     "ampersands",
     "ampersands",
+    "amphora",
     "anchor",
     "anchor",
     "angry",
     "angry",
     "annoyed",
     "annoyed",
@@ -241,6 +242,7 @@ LUCIDE_ICON_LIST = [
     "baggage_claim",
     "baggage_claim",
     "ban",
     "ban",
     "banana",
     "banana",
+    "bandage",
     "banknote",
     "banknote",
     "bar_chart",
     "bar_chart",
     "bar_chart_2",
     "bar_chart_2",
@@ -278,8 +280,10 @@ LUCIDE_ICON_LIST = [
     "between_horizontal_start",
     "between_horizontal_start",
     "between_vertical_end",
     "between_vertical_end",
     "between_vertical_start",
     "between_vertical_start",
+    "biceps_flexed",
     "bike",
     "bike",
     "binary",
     "binary",
+    "binoculars",
     "biohazard",
     "biohazard",
     "bird",
     "bird",
     "bitcoin",
     "bitcoin",
@@ -326,6 +330,7 @@ LUCIDE_ICON_LIST = [
     "boom_box",
     "boom_box",
     "bot",
     "bot",
     "bot_message_square",
     "bot_message_square",
+    "bot_off",
     "box",
     "box",
     "box_select",
     "box_select",
     "boxes",
     "boxes",
@@ -337,6 +342,7 @@ LUCIDE_ICON_LIST = [
     "brick_wall",
     "brick_wall",
     "briefcase",
     "briefcase",
     "briefcase_business",
     "briefcase_business",
+    "briefcase_conveyor_belt",
     "briefcase_medical",
     "briefcase_medical",
     "bring_to_front",
     "bring_to_front",
     "brush",
     "brush",
@@ -353,9 +359,13 @@ LUCIDE_ICON_LIST = [
     "cake_slice",
     "cake_slice",
     "calculator",
     "calculator",
     "calendar",
     "calendar",
+    "calendar_1",
+    "calendar_arrow_down",
+    "calendar_arrow_up",
     "calendar_check",
     "calendar_check",
     "calendar_check_2",
     "calendar_check_2",
     "calendar_clock",
     "calendar_clock",
+    "calendar_cog",
     "calendar_days",
     "calendar_days",
     "calendar_fold",
     "calendar_fold",
     "calendar_heart",
     "calendar_heart",
@@ -366,6 +376,7 @@ LUCIDE_ICON_LIST = [
     "calendar_plus_2",
     "calendar_plus_2",
     "calendar_range",
     "calendar_range",
     "calendar_search",
     "calendar_search",
+    "calendar_sync",
     "calendar_x",
     "calendar_x",
     "calendar_x_2",
     "calendar_x_2",
     "camera",
     "camera",
@@ -390,6 +401,29 @@ LUCIDE_ICON_LIST = [
     "castle",
     "castle",
     "cat",
     "cat",
     "cctv",
     "cctv",
+    "chart_area",
+    "chart_bar",
+    "chart_bar_big",
+    "chart_bar_decreasing",
+    "chart_bar_increasing",
+    "chart_bar_stacked",
+    "chart_candlestick",
+    "chart_column",
+    "chart_column_big",
+    "chart_column_decreasing",
+    "chart_column_increasing",
+    "chart_column_stacked",
+    "chart_gantt",
+    "chart_line",
+    "chart_network",
+    "chart_no_axes_column",
+    "chart_no_axes_column_decreasing",
+    "chart_no_axes_column_increasing",
+    "chart_no_axes_combined",
+    "chart_no_axes_gantt",
+    "chart_pie",
+    "chart_scatter",
+    "chart_spline",
     "check",
     "check",
     "check_check",
     "check_check",
     "chef_hat",
     "chef_hat",
@@ -404,6 +438,7 @@ LUCIDE_ICON_LIST = [
     "chevrons_down_up",
     "chevrons_down_up",
     "chevrons_left",
     "chevrons_left",
     "chevrons_left_right",
     "chevrons_left_right",
+    "chevrons_left_right_ellipsis",
     "chevrons_right",
     "chevrons_right",
     "chevrons_right_left",
     "chevrons_right_left",
     "chevrons_up",
     "chevrons_up",
@@ -422,8 +457,8 @@ LUCIDE_ICON_LIST = [
     "circle_arrow_out_up_right",
     "circle_arrow_out_up_right",
     "circle_arrow_right",
     "circle_arrow_right",
     "circle_arrow_up",
     "circle_arrow_up",
-    "circle_check_big",
     "circle_check",
     "circle_check",
+    "circle_check_big",
     "circle_chevron_down",
     "circle_chevron_down",
     "circle_chevron_left",
     "circle_chevron_left",
     "circle_chevron_right",
     "circle_chevron_right",
@@ -435,13 +470,14 @@ LUCIDE_ICON_LIST = [
     "circle_dot_dashed",
     "circle_dot_dashed",
     "circle_ellipsis",
     "circle_ellipsis",
     "circle_equal",
     "circle_equal",
+    "circle_fading_arrow_up",
     "circle_fading_plus",
     "circle_fading_plus",
     "circle_gauge",
     "circle_gauge",
     "circle_help",
     "circle_help",
     "circle_minus",
     "circle_minus",
     "circle_off",
     "circle_off",
-    "circle_parking_off",
     "circle_parking",
     "circle_parking",
+    "circle_parking_off",
     "circle_pause",
     "circle_pause",
     "circle_percent",
     "circle_percent",
     "circle_play",
     "circle_play",
@@ -480,7 +516,11 @@ LUCIDE_ICON_LIST = [
     "clock_7",
     "clock_7",
     "clock_8",
     "clock_8",
     "clock_9",
     "clock_9",
+    "clock_alert",
+    "clock_arrow_down",
+    "clock_arrow_up",
     "cloud",
     "cloud",
+    "cloud_alert",
     "cloud_cog",
     "cloud_cog",
     "cloud_download",
     "cloud_download",
     "cloud_drizzle",
     "cloud_drizzle",
@@ -551,6 +591,7 @@ LUCIDE_ICON_LIST = [
     "cup_soda",
     "cup_soda",
     "currency",
     "currency",
     "cylinder",
     "cylinder",
+    "dam",
     "database",
     "database",
     "database_backup",
     "database_backup",
     "database_zap",
     "database_zap",
@@ -558,7 +599,9 @@ LUCIDE_ICON_LIST = [
     "dessert",
     "dessert",
     "diameter",
     "diameter",
     "diamond",
     "diamond",
+    "diamond_minus",
     "diamond_percent",
     "diamond_percent",
+    "diamond_plus",
     "dice_1",
     "dice_1",
     "dice_2",
     "dice_2",
     "dice_3",
     "dice_3",
@@ -587,6 +630,7 @@ LUCIDE_ICON_LIST = [
     "dribbble",
     "dribbble",
     "drill",
     "drill",
     "droplet",
     "droplet",
+    "droplet_off",
     "droplets",
     "droplets",
     "drum",
     "drum",
     "drumstick",
     "drumstick",
@@ -602,12 +646,15 @@ LUCIDE_ICON_LIST = [
     "ellipsis",
     "ellipsis",
     "ellipsis_vertical",
     "ellipsis_vertical",
     "equal",
     "equal",
+    "equal_approximately",
     "equal_not",
     "equal_not",
     "eraser",
     "eraser",
+    "ethernet_port",
     "euro",
     "euro",
     "expand",
     "expand",
     "external_link",
     "external_link",
     "eye",
     "eye",
+    "eye_closed",
     "eye_off",
     "eye_off",
     "facebook",
     "facebook",
     "factory",
     "factory",
@@ -627,6 +674,10 @@ LUCIDE_ICON_LIST = [
     "file_bar_chart",
     "file_bar_chart",
     "file_bar_chart_2",
     "file_bar_chart_2",
     "file_box",
     "file_box",
+    "file_chart_column",
+    "file_chart_column_increasing",
+    "file_chart_line",
+    "file_chart_pie",
     "file_check",
     "file_check",
     "file_check_2",
     "file_check_2",
     "file_clock",
     "file_clock",
@@ -668,6 +719,7 @@ LUCIDE_ICON_LIST = [
     "file_type",
     "file_type",
     "file_type_2",
     "file_type_2",
     "file_up",
     "file_up",
+    "file_user",
     "file_video",
     "file_video",
     "file_video_2",
     "file_video_2",
     "file_volume",
     "file_volume",
@@ -709,6 +761,7 @@ LUCIDE_ICON_LIST = [
     "folder_check",
     "folder_check",
     "folder_clock",
     "folder_clock",
     "folder_closed",
     "folder_closed",
+    "folder_code",
     "folder_cog",
     "folder_cog",
     "folder_dot",
     "folder_dot",
     "folder_down",
     "folder_down",
@@ -781,7 +834,12 @@ LUCIDE_ICON_LIST = [
     "graduation_cap",
     "graduation_cap",
     "grape",
     "grape",
     "grid_2x2",
     "grid_2x2",
+    "grid_2x_2",
+    "grid_2x_2_check",
+    "grid_2x_2_plus",
+    "grid_2x_2_x",
     "grid_3x3",
     "grid_3x3",
+    "grid_3x_3",
     "grip",
     "grip",
     "grip_horizontal",
     "grip_horizontal",
     "grip_vertical",
     "grip_vertical",
@@ -810,6 +868,7 @@ LUCIDE_ICON_LIST = [
     "heading_4",
     "heading_4",
     "heading_5",
     "heading_5",
     "heading_6",
     "heading_6",
+    "headphone_off",
     "headphones",
     "headphones",
     "headset",
     "headset",
     "heart",
     "heart",
@@ -827,14 +886,20 @@ LUCIDE_ICON_LIST = [
     "hospital",
     "hospital",
     "hotel",
     "hotel",
     "hourglass",
     "hourglass",
+    "house",
+    "house_plug",
+    "house_plus",
     "ice_cream_bowl",
     "ice_cream_bowl",
     "ice_cream_cone",
     "ice_cream_cone",
+    "id_card",
     "image",
     "image",
     "image_down",
     "image_down",
     "image_minus",
     "image_minus",
     "image_off",
     "image_off",
+    "image_play",
     "image_plus",
     "image_plus",
     "image_up",
     "image_up",
+    "image_upscale",
     "images",
     "images",
     "import",
     "import",
     "inbox",
     "inbox",
@@ -856,6 +921,7 @@ LUCIDE_ICON_LIST = [
     "key_square",
     "key_square",
     "keyboard",
     "keyboard",
     "keyboard_music",
     "keyboard_music",
+    "keyboard_off",
     "lamp",
     "lamp",
     "lamp_ceiling",
     "lamp_ceiling",
     "lamp_desk",
     "lamp_desk",
@@ -865,8 +931,9 @@ LUCIDE_ICON_LIST = [
     "land_plot",
     "land_plot",
     "landmark",
     "landmark",
     "languages",
     "languages",
-    "laptop_minimal",
     "laptop",
     "laptop",
+    "laptop_minimal",
+    "laptop_minimal_check",
     "lasso",
     "lasso",
     "lasso_select",
     "lasso_select",
     "laugh",
     "laugh",
@@ -881,6 +948,8 @@ LUCIDE_ICON_LIST = [
     "layout_template",
     "layout_template",
     "leaf",
     "leaf",
     "leafy_green",
     "leafy_green",
+    "lectern",
+    "letter_text",
     "library",
     "library",
     "library_big",
     "library_big",
     "life_buoy",
     "life_buoy",
@@ -893,10 +962,12 @@ LUCIDE_ICON_LIST = [
     "link_2_off",
     "link_2_off",
     "linkedin",
     "linkedin",
     "list",
     "list",
+    "list_check",
     "list_checks",
     "list_checks",
     "list_collapse",
     "list_collapse",
     "list_end",
     "list_end",
     "list_filter",
     "list_filter",
+    "list_filter_plus",
     "list_minus",
     "list_minus",
     "list_music",
     "list_music",
     "list_ordered",
     "list_ordered",
@@ -909,15 +980,17 @@ LUCIDE_ICON_LIST = [
     "list_x",
     "list_x",
     "loader",
     "loader",
     "loader_circle",
     "loader_circle",
+    "loader_pinwheel",
     "locate",
     "locate",
     "locate_fixed",
     "locate_fixed",
     "locate_off",
     "locate_off",
     "lock",
     "lock",
-    "lock_keyhole_open",
     "lock_keyhole",
     "lock_keyhole",
+    "lock_keyhole_open",
     "lock_open",
     "lock_open",
     "log_in",
     "log_in",
     "log_out",
     "log_out",
+    "logs",
     "lollipop",
     "lollipop",
     "luggage",
     "luggage",
     "magnet",
     "magnet",
@@ -934,7 +1007,16 @@ LUCIDE_ICON_LIST = [
     "mails",
     "mails",
     "map",
     "map",
     "map_pin",
     "map_pin",
+    "map_pin_check",
+    "map_pin_check_inside",
+    "map_pin_house",
+    "map_pin_minus",
+    "map_pin_minus_inside",
     "map_pin_off",
     "map_pin_off",
+    "map_pin_plus",
+    "map_pin_plus_inside",
+    "map_pin_x",
+    "map_pin_x_inside",
     "map_pinned",
     "map_pinned",
     "martini",
     "martini",
     "maximize",
     "maximize",
@@ -963,6 +1045,7 @@ LUCIDE_ICON_LIST = [
     "message_square_diff",
     "message_square_diff",
     "message_square_dot",
     "message_square_dot",
     "message_square_heart",
     "message_square_heart",
+    "message_square_lock",
     "message_square_more",
     "message_square_more",
     "message_square_off",
     "message_square_off",
     "message_square_plus",
     "message_square_plus",
@@ -974,8 +1057,9 @@ LUCIDE_ICON_LIST = [
     "message_square_x",
     "message_square_x",
     "messages_square",
     "messages_square",
     "mic",
     "mic",
-    "mic_vocal",
     "mic_off",
     "mic_off",
+    "mic_vocal",
+    "microchip",
     "microscope",
     "microscope",
     "microwave",
     "microwave",
     "milestone",
     "milestone",
@@ -986,6 +1070,7 @@ LUCIDE_ICON_LIST = [
     "minus",
     "minus",
     "monitor",
     "monitor",
     "monitor_check",
     "monitor_check",
+    "monitor_cog",
     "monitor_dot",
     "monitor_dot",
     "monitor_down",
     "monitor_down",
     "monitor_off",
     "monitor_off",
@@ -1001,8 +1086,10 @@ LUCIDE_ICON_LIST = [
     "mountain",
     "mountain",
     "mountain_snow",
     "mountain_snow",
     "mouse",
     "mouse",
+    "mouse_off",
     "mouse_pointer",
     "mouse_pointer",
     "mouse_pointer_2",
     "mouse_pointer_2",
+    "mouse_pointer_ban",
     "mouse_pointer_click",
     "mouse_pointer_click",
     "move",
     "move",
     "move_3d",
     "move_3d",
@@ -1039,10 +1126,13 @@ LUCIDE_ICON_LIST = [
     "nut_off",
     "nut_off",
     "octagon",
     "octagon",
     "octagon_alert",
     "octagon_alert",
+    "octagon_minus",
     "octagon_pause",
     "octagon_pause",
     "octagon_x",
     "octagon_x",
+    "omega",
     "option",
     "option",
     "orbit",
     "orbit",
+    "origami",
     "package",
     "package",
     "package_2",
     "package_2",
     "package_check",
     "package_check",
@@ -1055,6 +1145,7 @@ LUCIDE_ICON_LIST = [
     "paint_roller",
     "paint_roller",
     "paintbrush",
     "paintbrush",
     "paintbrush_2",
     "paintbrush_2",
+    "paintbrush_vertical",
     "palette",
     "palette",
     "panel_bottom",
     "panel_bottom",
     "panel_bottom_close",
     "panel_bottom_close",
@@ -1084,13 +1175,16 @@ LUCIDE_ICON_LIST = [
     "pc_case",
     "pc_case",
     "pen",
     "pen",
     "pen_line",
     "pen_line",
+    "pen_off",
     "pen_tool",
     "pen_tool",
     "pencil",
     "pencil",
     "pencil_line",
     "pencil_line",
+    "pencil_off",
     "pencil_ruler",
     "pencil_ruler",
     "pentagon",
     "pentagon",
     "percent",
     "percent",
     "person_standing",
     "person_standing",
+    "philippine_peso",
     "phone",
     "phone",
     "phone_call",
     "phone_call",
     "phone_forwarded",
     "phone_forwarded",
@@ -1106,7 +1200,10 @@ LUCIDE_ICON_LIST = [
     "pie_chart",
     "pie_chart",
     "piggy_bank",
     "piggy_bank",
     "pilcrow",
     "pilcrow",
+    "pilcrow_left",
+    "pilcrow_right",
     "pill",
     "pill",
+    "pill_bottle",
     "pin",
     "pin",
     "pin_off",
     "pin_off",
     "pipette",
     "pipette",
@@ -1132,6 +1229,7 @@ LUCIDE_ICON_LIST = [
     "power_off",
     "power_off",
     "presentation",
     "presentation",
     "printer",
     "printer",
+    "printer_check",
     "projector",
     "projector",
     "proportions",
     "proportions",
     "puzzle",
     "puzzle",
@@ -1206,6 +1304,7 @@ LUCIDE_ICON_LIST = [
     "satellite_dish",
     "satellite_dish",
     "save",
     "save",
     "save_all",
     "save_all",
+    "save_off",
     "scale",
     "scale",
     "scale_3d",
     "scale_3d",
     "scaling",
     "scaling",
@@ -1213,7 +1312,9 @@ LUCIDE_ICON_LIST = [
     "scan_barcode",
     "scan_barcode",
     "scan_eye",
     "scan_eye",
     "scan_face",
     "scan_face",
+    "scan_heart",
     "scan_line",
     "scan_line",
+    "scan_qr_code",
     "scan_search",
     "scan_search",
     "scan_text",
     "scan_text",
     "scatter_chart",
     "scatter_chart",
@@ -1229,6 +1330,7 @@ LUCIDE_ICON_LIST = [
     "search_code",
     "search_code",
     "search_slash",
     "search_slash",
     "search_x",
     "search_x",
+    "section",
     "send",
     "send",
     "send_horizontal",
     "send_horizontal",
     "send_to_back",
     "send_to_back",
@@ -1273,6 +1375,7 @@ LUCIDE_ICON_LIST = [
     "signal_low",
     "signal_low",
     "signal_medium",
     "signal_medium",
     "signal_zero",
     "signal_zero",
+    "signature",
     "signpost",
     "signpost",
     "signpost_big",
     "signpost_big",
     "siren",
     "siren",
@@ -1282,8 +1385,8 @@ LUCIDE_ICON_LIST = [
     "slack",
     "slack",
     "slash",
     "slash",
     "slice",
     "slice",
-    "sliders_vertical",
     "sliders_horizontal",
     "sliders_horizontal",
+    "sliders_vertical",
     "smartphone",
     "smartphone",
     "smartphone_charging",
     "smartphone_charging",
     "smartphone_nfc",
     "smartphone_nfc",
@@ -1307,29 +1410,31 @@ LUCIDE_ICON_LIST = [
     "sprout",
     "sprout",
     "square",
     "square",
     "square_activity",
     "square_activity",
+    "square_arrow_down",
     "square_arrow_down_left",
     "square_arrow_down_left",
     "square_arrow_down_right",
     "square_arrow_down_right",
-    "square_arrow_down",
     "square_arrow_left",
     "square_arrow_left",
     "square_arrow_out_down_left",
     "square_arrow_out_down_left",
     "square_arrow_out_down_right",
     "square_arrow_out_down_right",
     "square_arrow_out_up_left",
     "square_arrow_out_up_left",
     "square_arrow_out_up_right",
     "square_arrow_out_up_right",
     "square_arrow_right",
     "square_arrow_right",
+    "square_arrow_up",
     "square_arrow_up_left",
     "square_arrow_up_left",
     "square_arrow_up_right",
     "square_arrow_up_right",
-    "square_arrow_up",
     "square_asterisk",
     "square_asterisk",
     "square_bottom_dashed_scissors",
     "square_bottom_dashed_scissors",
-    "square_check_big",
+    "square_chart_gantt",
     "square_check",
     "square_check",
+    "square_check_big",
     "square_chevron_down",
     "square_chevron_down",
     "square_chevron_left",
     "square_chevron_left",
     "square_chevron_right",
     "square_chevron_right",
     "square_chevron_up",
     "square_chevron_up",
     "square_code",
     "square_code",
-    "square_dashed_bottom_code",
+    "square_dashed",
     "square_dashed_bottom",
     "square_dashed_bottom",
+    "square_dashed_bottom_code",
     "square_dashed_kanban",
     "square_dashed_kanban",
     "square_dashed_mouse_pointer",
     "square_dashed_mouse_pointer",
     "square_divide",
     "square_divide",
@@ -1343,8 +1448,8 @@ LUCIDE_ICON_LIST = [
     "square_menu",
     "square_menu",
     "square_minus",
     "square_minus",
     "square_mouse_pointer",
     "square_mouse_pointer",
-    "square_parking_off",
     "square_parking",
     "square_parking",
+    "square_parking_off",
     "square_pen",
     "square_pen",
     "square_percent",
     "square_percent",
     "square_pi",
     "square_pi",
@@ -1358,10 +1463,11 @@ LUCIDE_ICON_LIST = [
     "square_slash",
     "square_slash",
     "square_split_horizontal",
     "square_split_horizontal",
     "square_split_vertical",
     "square_split_vertical",
+    "square_square",
     "square_stack",
     "square_stack",
     "square_terminal",
     "square_terminal",
-    "square_user_round",
     "square_user",
     "square_user",
+    "square_user_round",
     "square_x",
     "square_x",
     "squircle",
     "squircle",
     "squirrel",
     "squirrel",
@@ -1398,6 +1504,7 @@ LUCIDE_ICON_LIST = [
     "table_cells_merge",
     "table_cells_merge",
     "table_cells_split",
     "table_cells_split",
     "table_columns_split",
     "table_columns_split",
+    "table_of_contents",
     "table_properties",
     "table_properties",
     "table_rows_split",
     "table_rows_split",
     "tablet",
     "tablet",
@@ -1413,11 +1520,11 @@ LUCIDE_ICON_LIST = [
     "tangent",
     "tangent",
     "target",
     "target",
     "telescope",
     "telescope",
+    "tent",
     "tent_tree",
     "tent_tree",
     "terminal",
     "terminal",
-    "test_tube_diagonal",
     "test_tube",
     "test_tube",
-    "tent",
+    "test_tube_diagonal",
     "test_tubes",
     "test_tubes",
     "text",
     "text",
     "text_cursor",
     "text_cursor",
@@ -1438,11 +1545,14 @@ LUCIDE_ICON_LIST = [
     "ticket_plus",
     "ticket_plus",
     "ticket_slash",
     "ticket_slash",
     "ticket_x",
     "ticket_x",
+    "tickets",
+    "tickets_plane",
     "timer",
     "timer",
     "timer_off",
     "timer_off",
     "timer_reset",
     "timer_reset",
     "toggle_left",
     "toggle_left",
     "toggle_right",
     "toggle_right",
+    "toilet",
     "tornado",
     "tornado",
     "torus",
     "torus",
     "touchpad",
     "touchpad",
@@ -1464,17 +1574,21 @@ LUCIDE_ICON_LIST = [
     "trello",
     "trello",
     "trending_down",
     "trending_down",
     "trending_up",
     "trending_up",
+    "trending_up_down",
     "triangle",
     "triangle",
-    "triangle_right",
     "triangle_alert",
     "triangle_alert",
+    "triangle_right",
     "trophy",
     "trophy",
     "truck",
     "truck",
     "turtle",
     "turtle",
     "tv",
     "tv",
     "tv_2",
     "tv_2",
+    "tv_minimal",
+    "tv_minimal_play",
     "twitch",
     "twitch",
     "twitter",
     "twitter",
     "type",
     "type",
+    "type_outline",
     "umbrella",
     "umbrella",
     "umbrella_off",
     "umbrella_off",
     "underline",
     "underline",
@@ -1485,8 +1599,8 @@ LUCIDE_ICON_LIST = [
     "unfold_vertical",
     "unfold_vertical",
     "ungroup",
     "ungroup",
     "university",
     "university",
-    "unlink_2",
     "unlink",
     "unlink",
+    "unlink_2",
     "unplug",
     "unplug",
     "upload",
     "upload",
     "usb",
     "usb",
@@ -1494,11 +1608,13 @@ LUCIDE_ICON_LIST = [
     "user_check",
     "user_check",
     "user_cog",
     "user_cog",
     "user_minus",
     "user_minus",
+    "user_pen",
     "user_plus",
     "user_plus",
     "user_round",
     "user_round",
     "user_round_check",
     "user_round_check",
     "user_round_cog",
     "user_round_cog",
     "user_round_minus",
     "user_round_minus",
+    "user_round_pen",
     "user_round_plus",
     "user_round_plus",
     "user_round_search",
     "user_round_search",
     "user_round_x",
     "user_round_x",
@@ -1520,14 +1636,16 @@ LUCIDE_ICON_LIST = [
     "videotape",
     "videotape",
     "view",
     "view",
     "voicemail",
     "voicemail",
+    "volleyball",
     "volume",
     "volume",
     "volume_1",
     "volume_1",
     "volume_2",
     "volume_2",
+    "volume_off",
     "volume_x",
     "volume_x",
     "vote",
     "vote",
     "wallet",
     "wallet",
-    "wallet_minimal",
     "wallet_cards",
     "wallet_cards",
+    "wallet_minimal",
     "wallpaper",
     "wallpaper",
     "wand",
     "wand",
     "wand_sparkles",
     "wand_sparkles",
@@ -1535,17 +1653,22 @@ LUCIDE_ICON_LIST = [
     "washing_machine",
     "washing_machine",
     "watch",
     "watch",
     "waves",
     "waves",
+    "waves_ladder",
     "waypoints",
     "waypoints",
     "webcam",
     "webcam",
-    "webhook_off",
     "webhook",
     "webhook",
+    "webhook_off",
     "weight",
     "weight",
     "wheat",
     "wheat",
     "wheat_off",
     "wheat_off",
     "whole_word",
     "whole_word",
     "wifi",
     "wifi",
+    "wifi_high",
+    "wifi_low",
     "wifi_off",
     "wifi_off",
+    "wifi_zero",
     "wind",
     "wind",
+    "wind_arrow_down",
     "wine",
     "wine",
     "wine_off",
     "wine_off",
     "workflow",
     "workflow",
@@ -1559,3 +1682,7 @@ LUCIDE_ICON_LIST = [
     "zoom_in",
     "zoom_in",
     "zoom_out",
     "zoom_out",
 ]
 ]
+LUCIDE_ICON_MAPPING_OVERRIDE = {
+    "grid_2x_2_check": "Grid2x2Check",
+    "grid_2x_2_x": "Grid2x2X",
+}

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

@@ -420,11 +420,12 @@ const {_LANGUAGE!s} = match ? match[1] : '';
 
 
     def _get_custom_code(self) -> str | None:
     def _get_custom_code(self) -> str | None:
         hooks = {}
         hooks = {}
+        from reflex.compiler.templates import MACROS
+
         for _component in self.component_map.values():
         for _component in self.component_map.values():
             comp = _component(_MOCK_ARG)
             comp = _component(_MOCK_ARG)
-            hooks.update(comp._get_all_hooks_internal())
             hooks.update(comp._get_all_hooks())
             hooks.update(comp._get_all_hooks())
-        formatted_hooks = "\n".join(hooks.keys())
+        formatted_hooks = MACROS.module.renderHooks(hooks)  # type: ignore
         return f"""
         return f"""
         function {self._get_component_map_name()} () {{
         function {self._get_component_map_name()} () {{
             {formatted_hooks}
             {formatted_hooks}

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

@@ -151,8 +151,8 @@ class ColorModeIconButton(IconButton):
                 dropdown_menu.trigger(
                 dropdown_menu.trigger(
                     super().create(
                     super().create(
                         ColorModeIcon.create(),
                         ColorModeIcon.create(),
-                        **props,
-                    )
+                    ),
+                    **props,
                 ),
                 ),
                 dropdown_menu.content(
                 dropdown_menu.content(
                     color_mode_item("light"),
                     color_mode_item("light"),

+ 1 - 1
reflex/components/radix/themes/typography/link.py

@@ -76,7 +76,7 @@ class Link(RadixThemesComponent, A, MemoizationLeaf, MarkdownComponentMap):
         Returns:
         Returns:
             Component: The link component
             Component: The link component
         """
         """
-        props.setdefault(":hover", {"color": color("accent", 8)})
+        props.setdefault("_hover", {"color": color("accent", 8)})
         href = props.get("href")
         href = props.get("href")
 
 
         is_external = props.pop("is_external", None)
         is_external = props.pop("is_external", None)

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

@@ -85,8 +85,8 @@ class ChartBase(RechartsCharts):
         cls._ensure_valid_dimension("height", height)
         cls._ensure_valid_dimension("height", height)
 
 
         dim_props = {
         dim_props = {
-            "width": width or "100%",
-            "height": height or "100%",
+            "width": width if width is not None else "100%",
+            "height": height if height is not None else "100%",
         }
         }
         # Provide min dimensions so the graph always appears, even if the outer container is zero-size.
         # Provide min dimensions so the graph always appears, even if the outer container is zero-size.
         if width is None:
         if width is None:

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

@@ -167,7 +167,7 @@ class ToastProps(PropsBase, NoExtrasAllowedProps):
 class Toaster(Component):
 class Toaster(Component):
     """A Toaster Component for displaying toast notifications."""
     """A Toaster Component for displaying toast notifications."""
 
 
-    library: str = "sonner@1.5.0"
+    library: str = "sonner@1.7.1"
 
 
     tag = "Toaster"
     tag = "Toaster"
 
 

+ 1 - 0
reflex/constants/compiler.py

@@ -135,6 +135,7 @@ class Hooks(SimpleNamespace):
     class HookPosition(enum.Enum):
     class HookPosition(enum.Enum):
         """The position of the hook in the component."""
         """The position of the hook in the component."""
 
 
+        INTERNAL = "internal"
         PRE_TRIGGER = "pre_trigger"
         PRE_TRIGGER = "pre_trigger"
         POST_TRIGGER = "post_trigger"
         POST_TRIGGER = "post_trigger"
 
 

+ 98 - 1
reflex/event.py

@@ -91,6 +91,8 @@ class Event:
         return f"{self.token}_{substate}"
         return f"{self.token}_{substate}"
 
 
 
 
+_EVENT_FIELDS: set[str] = {f.name for f in dataclasses.fields(Event)}
+
 BACKGROUND_TASK_MARKER = "_reflex_background_task"
 BACKGROUND_TASK_MARKER = "_reflex_background_task"
 
 
 
 
@@ -431,6 +433,101 @@ class EventChain(EventActionsMixin):
 
 
     invocation: Optional[Var] = dataclasses.field(default=None)
     invocation: Optional[Var] = dataclasses.field(default=None)
 
 
+    @classmethod
+    def create(
+        cls,
+        value: EventType,
+        args_spec: ArgsSpec | Sequence[ArgsSpec],
+        key: Optional[str] = None,
+        **event_chain_kwargs,
+    ) -> Union[EventChain, Var]:
+        """Create an event chain from a variety of input types.
+
+        Args:
+            value: The value to create the event chain from.
+            args_spec: The args_spec of the event trigger being bound.
+            key: The key of the event trigger being bound.
+            **event_chain_kwargs: Additional kwargs to pass to the EventChain constructor.
+
+        Returns:
+            The event chain.
+
+        Raises:
+            ValueError: If the value is not a valid event chain.
+        """
+        # If it's an event chain var, return it.
+        if isinstance(value, Var):
+            if isinstance(value, EventChainVar):
+                return value
+            elif isinstance(value, EventVar):
+                value = [value]
+            elif issubclass(value._var_type, (EventChain, EventSpec)):
+                return cls.create(
+                    value=value.guess_type(),
+                    args_spec=args_spec,
+                    key=key,
+                    **event_chain_kwargs,
+                )
+            else:
+                raise ValueError(
+                    f"Invalid event chain: {value!s} of type {value._var_type}"
+                )
+        elif isinstance(value, EventChain):
+            # Trust that the caller knows what they're doing passing an EventChain directly
+            return value
+
+        # If the input is a single event handler, wrap it in a list.
+        if isinstance(value, (EventHandler, EventSpec)):
+            value = [value]
+
+        # If the input is a list of event handlers, create an event chain.
+        if isinstance(value, List):
+            events: List[Union[EventSpec, EventVar]] = []
+            for v in value:
+                if isinstance(v, (EventHandler, EventSpec)):
+                    # Call the event handler to get the event.
+                    events.append(call_event_handler(v, args_spec, key=key))
+                elif isinstance(v, Callable):
+                    # Call the lambda to get the event chain.
+                    result = call_event_fn(v, args_spec, key=key)
+                    if isinstance(result, Var):
+                        raise ValueError(
+                            f"Invalid event chain: {v}. Cannot use a Var-returning "
+                            "lambda inside an EventChain list."
+                        )
+                    events.extend(result)
+                elif isinstance(v, EventVar):
+                    events.append(v)
+                else:
+                    raise ValueError(f"Invalid event: {v}")
+
+        # If the input is a callable, create an event chain.
+        elif isinstance(value, Callable):
+            result = call_event_fn(value, args_spec, key=key)
+            if isinstance(result, Var):
+                # Recursively call this function if the lambda returned an EventChain Var.
+                return cls.create(
+                    value=result, args_spec=args_spec, key=key, **event_chain_kwargs
+                )
+            events = [*result]
+
+        # Otherwise, raise an error.
+        else:
+            raise ValueError(f"Invalid event chain: {value}")
+
+        # Add args to the event specs if necessary.
+        events = [
+            (e.with_args(get_handler_args(e)) if isinstance(e, EventSpec) else e)
+            for e in events
+        ]
+
+        # Return the event chain.
+        return cls(
+            events=events,
+            args_spec=args_spec,
+            **event_chain_kwargs,
+        )
+
 
 
 @dataclasses.dataclass(
 @dataclasses.dataclass(
     init=True,
     init=True,
@@ -1100,7 +1197,7 @@ def call_function(
     Returns:
     Returns:
         EventSpec: An event that will execute the client side javascript.
         EventSpec: An event that will execute the client side javascript.
     """
     """
-    callback_kwargs = {}
+    callback_kwargs = {"callback": None}
     if callback is not None:
     if callback is not None:
         callback_kwargs = {
         callback_kwargs = {
             "callback": format.format_queue_events(
             "callback": format.format_queue_events(

+ 42 - 20
reflex/experimental/client_state.py

@@ -12,7 +12,7 @@ from reflex.event import EventChain, EventHandler, EventSpec, run_script
 from reflex.utils.imports import ImportVar
 from reflex.utils.imports import ImportVar
 from reflex.vars import VarData, get_unique_variable_name
 from reflex.vars import VarData, get_unique_variable_name
 from reflex.vars.base import LiteralVar, Var
 from reflex.vars.base import LiteralVar, Var
-from reflex.vars.function import FunctionVar
+from reflex.vars.function import ArgsFunctionOperationBuilder, FunctionVar
 
 
 NoValue = object()
 NoValue = object()
 
 
@@ -45,6 +45,7 @@ class ClientStateVar(Var):
     # Track the names of the getters and setters
     # Track the names of the getters and setters
     _setter_name: str = dataclasses.field(default="")
     _setter_name: str = dataclasses.field(default="")
     _getter_name: str = dataclasses.field(default="")
     _getter_name: str = dataclasses.field(default="")
+    _id_name: str = dataclasses.field(default="")
 
 
     # Whether to add the var and setter to the global `refs` object for use in any Component.
     # Whether to add the var and setter to the global `refs` object for use in any Component.
     _global_ref: bool = dataclasses.field(default=True)
     _global_ref: bool = dataclasses.field(default=True)
@@ -96,6 +97,7 @@ class ClientStateVar(Var):
         """
         """
         if var_name is None:
         if var_name is None:
             var_name = get_unique_variable_name()
             var_name = get_unique_variable_name()
+        id_name = "id_" + get_unique_variable_name()
         if not isinstance(var_name, str):
         if not isinstance(var_name, str):
             raise ValueError("var_name must be a string.")
             raise ValueError("var_name must be a string.")
         if default is NoValue:
         if default is NoValue:
@@ -105,20 +107,24 @@ class ClientStateVar(Var):
         else:
         else:
             default_var = default
             default_var = default
         setter_name = f"set{var_name.capitalize()}"
         setter_name = f"set{var_name.capitalize()}"
-        hooks = {
+        hooks: dict[str, VarData | None] = {
+            f"const {id_name} = useId()": None,
             f"const [{var_name}, {setter_name}] = useState({default_var!s})": None,
             f"const [{var_name}, {setter_name}] = useState({default_var!s})": None,
         }
         }
         imports = {
         imports = {
-            "react": [ImportVar(tag="useState")],
+            "react": [ImportVar(tag="useState"), ImportVar(tag="useId")],
         }
         }
         if global_ref:
         if global_ref:
-            hooks[f"{_client_state_ref(var_name)} = {var_name}"] = None
-            hooks[f"{_client_state_ref(setter_name)} = {setter_name}"] = None
+            hooks[f"{_client_state_ref(var_name)} ??= {{}}"] = None
+            hooks[f"{_client_state_ref(setter_name)} ??= {{}}"] = None
+            hooks[f"{_client_state_ref(var_name)}[{id_name}] = {var_name}"] = None
+            hooks[f"{_client_state_ref(setter_name)}[{id_name}] = {setter_name}"] = None
             imports.update(_refs_import)
             imports.update(_refs_import)
         return cls(
         return cls(
             _js_expr="",
             _js_expr="",
             _setter_name=setter_name,
             _setter_name=setter_name,
             _getter_name=var_name,
             _getter_name=var_name,
+            _id_name=id_name,
             _global_ref=global_ref,
             _global_ref=global_ref,
             _var_type=default_var._var_type,
             _var_type=default_var._var_type,
             _var_data=VarData.merge(
             _var_data=VarData.merge(
@@ -144,10 +150,11 @@ class ClientStateVar(Var):
         return (
         return (
             Var(
             Var(
                 _js_expr=(
                 _js_expr=(
-                    _client_state_ref(self._getter_name)
+                    _client_state_ref(self._getter_name) + f"[{self._id_name}]"
                     if self._global_ref
                     if self._global_ref
                     else self._getter_name
                     else self._getter_name
-                )
+                ),
+                _var_data=self._var_data,
             )
             )
             .to(self._var_type)
             .to(self._var_type)
             ._replace(
             ._replace(
@@ -170,28 +177,43 @@ class ClientStateVar(Var):
         Returns:
         Returns:
             A special EventChain Var which will set the value when triggered.
             A special EventChain Var which will set the value when triggered.
         """
         """
+        _var_data = VarData(imports=_refs_import if self._global_ref else {})
+
+        arg_name = get_unique_variable_name()
         setter = (
         setter = (
-            _client_state_ref(self._setter_name)
+            ArgsFunctionOperationBuilder.create(
+                args_names=(arg_name,),
+                return_expr=Var("Array.prototype.forEach.call")
+                .to(FunctionVar)
+                .call(
+                    Var("Object.values")
+                    .to(FunctionVar)
+                    .call(Var(_client_state_ref(self._setter_name))),
+                    ArgsFunctionOperationBuilder.create(
+                        args_names=("setter",),
+                        return_expr=Var("setter").to(FunctionVar).call(Var(arg_name)),
+                    ),
+                ),
+                _var_data=_var_data,
+            )
             if self._global_ref
             if self._global_ref
-            else self._setter_name
+            else Var(self._setter_name, _var_data=_var_data).to(FunctionVar)
         )
         )
-        _var_data = VarData(imports=_refs_import if self._global_ref else {})
+
         if value is not NoValue:
         if value is not NoValue:
             # This is a hack to make it work like an EventSpec taking an arg
             # This is a hack to make it work like an EventSpec taking an arg
             value_var = LiteralVar.create(value)
             value_var = LiteralVar.create(value)
-            _var_data = VarData.merge(_var_data, value_var._get_all_var_data())
             value_str = str(value_var)
             value_str = str(value_var)
 
 
-            if value_str.startswith("_"):
+            setter = ArgsFunctionOperationBuilder.create(
                 # remove patterns of ["*"] from the value_str using regex
                 # remove patterns of ["*"] from the value_str using regex
-                arg = re.sub(r"\[\".*\"\]", "", value_str)
-                setter = f"(({arg}) => {setter}({value_str}))"
-            else:
-                setter = f"(() => {setter}({value_str}))"
-        return Var(
-            _js_expr=setter,
-            _var_data=_var_data,
-        ).to(FunctionVar, EventChain)
+                args_names=(re.sub(r"\[\".*\"\]", "", value_str),)
+                if value_str.startswith("_")
+                else (),
+                return_expr=setter.call(value_var),
+            )
+
+        return setter.to(FunctionVar, EventChain)
 
 
     @property
     @property
     def set(self) -> Var:
     def set(self) -> Var:

+ 1 - 0
reflex/model.py

@@ -533,6 +533,7 @@ def asession(url: str | None = None) -> AsyncSession:
         _AsyncSessionLocal[url] = sqlalchemy.ext.asyncio.async_sessionmaker(
         _AsyncSessionLocal[url] = sqlalchemy.ext.asyncio.async_sessionmaker(
             bind=get_async_engine(url),
             bind=get_async_engine(url),
             class_=AsyncSession,
             class_=AsyncSession,
+            expire_on_commit=False,
             autocommit=False,
             autocommit=False,
             autoflush=False,
             autoflush=False,
         )
         )

+ 6 - 0
reflex/reflex.py

@@ -485,6 +485,11 @@ def deploy(
         "--token",
         "--token",
         help="token to use for auth",
         help="token to use for auth",
     ),
     ),
+    config_path: Optional[str] = typer.Option(
+        None,
+        "--config",
+        help="path to the config file",
+    ),
 ):
 ):
     """Deploy the app to the Reflex hosting service."""
     """Deploy the app to the Reflex hosting service."""
     from reflex_cli.utils import dependency
     from reflex_cli.utils import dependency
@@ -540,6 +545,7 @@ def deploy(
         loglevel=type(loglevel).INFO,  # type: ignore
         loglevel=type(loglevel).INFO,  # type: ignore
         token=token,
         token=token,
         project=project,
         project=project,
+        config_path=config_path,
     )
     )
 
 
 
 

+ 107 - 2
reflex/state.py

@@ -107,6 +107,7 @@ from reflex.utils.exceptions import (
     StateSchemaMismatchError,
     StateSchemaMismatchError,
     StateSerializationError,
     StateSerializationError,
     StateTooLargeError,
     StateTooLargeError,
+    UnretrievableVarValueError,
 )
 )
 from reflex.utils.exec import is_testing_env
 from reflex.utils.exec import is_testing_env
 from reflex.utils.serializers import serializer
 from reflex.utils.serializers import serializer
@@ -143,6 +144,9 @@ HANDLED_PICKLE_ERRORS = (
     ValueError,
     ValueError,
 )
 )
 
 
+# For BaseState.get_var_value
+VAR_TYPE = TypeVar("VAR_TYPE")
+
 
 
 def _no_chain_background_task(
 def _no_chain_background_task(
     state_cls: Type["BaseState"], name: str, fn: Callable
     state_cls: Type["BaseState"], name: str, fn: Callable
@@ -1193,6 +1197,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
                 continue
                 continue
             dynamic_vars[param] = DynamicRouteVar(
             dynamic_vars[param] = DynamicRouteVar(
                 fget=func,
                 fget=func,
+                auto_deps=False,
+                deps=["router"],
                 cache=True,
                 cache=True,
                 _js_expr=param,
                 _js_expr=param,
                 _var_data=VarData.from_state(cls),
                 _var_data=VarData.from_state(cls),
@@ -1598,6 +1604,42 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
         # Slow case - fetch missing parent states from redis.
         # Slow case - fetch missing parent states from redis.
         return await self._get_state_from_redis(state_cls)
         return await self._get_state_from_redis(state_cls)
 
 
+    async def get_var_value(self, var: Var[VAR_TYPE]) -> VAR_TYPE:
+        """Get the value of an rx.Var from another state.
+
+        Args:
+            var: The var to get the value for.
+
+        Returns:
+            The value of the var.
+
+        Raises:
+            UnretrievableVarValueError: If the var does not have a literal value
+                or associated state.
+        """
+        # Oopsie case: you didn't give me a Var... so get what you give.
+        if not isinstance(var, Var):
+            return var  # type: ignore
+
+        # Fast case: this is a literal var and the value is known.
+        if hasattr(var, "_var_value"):
+            return var._var_value
+
+        var_data = var._get_all_var_data()
+        if var_data is None or not var_data.state:
+            raise UnretrievableVarValueError(
+                f"Unable to retrieve value for {var._js_expr}: not associated with any state."
+            )
+        # Fastish case: this var belongs to this state
+        if var_data.state == self.get_full_name():
+            return getattr(self, var_data.field_name)
+
+        # Slow case: this var belongs to another state
+        other_state = await self.get_state(
+            self._get_root_state().get_class_substate(var_data.state)
+        )
+        return getattr(other_state, var_data.field_name)
+
     def _get_event_handler(
     def _get_event_handler(
         self, event: Event
         self, event: Event
     ) -> tuple[BaseState | StateProxy, EventHandler]:
     ) -> tuple[BaseState | StateProxy, EventHandler]:
@@ -3647,6 +3689,9 @@ def get_state_manager() -> StateManager:
 class MutableProxy(wrapt.ObjectProxy):
 class MutableProxy(wrapt.ObjectProxy):
     """A proxy for a mutable object that tracks changes."""
     """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.
     # Methods on wrapped objects which should mark the state as dirty.
     __mark_dirty_attrs__ = {
     __mark_dirty_attrs__ = {
         "add",
         "add",
@@ -3689,6 +3734,39 @@ class MutableProxy(wrapt.ObjectProxy):
         BaseModelV1,
         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 [reportGeneralTypeIssues]
+                            wrapped_cls,
+                            dataclasses._FIELDS,  # pyright: ignore [reportGeneralTypeIssues]
+                        ),
+                    },
+                )
+            cls = cls.__dataclass_proxies__[wrapper_cls_name]
+        return super().__new__(cls)
+
     def __init__(self, wrapped: Any, state: BaseState, field_name: str):
     def __init__(self, wrapped: Any, state: BaseState, field_name: str):
         """Create a proxy for a mutable object that tracks changes.
         """Create a proxy for a mutable object that tracks changes.
 
 
@@ -3745,7 +3823,27 @@ class MutableProxy(wrapt.ObjectProxy):
         Returns:
         Returns:
             Whether the value is of a mutable type.
             Whether the value is of a mutable type.
         """
         """
-        return isinstance(value, cls.__mutable_types__)
+        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:
     def _wrap_recursive(self, value: Any) -> Any:
         """Wrap a value recursively if it is mutable.
         """Wrap a value recursively if it is mutable.
@@ -3756,9 +3854,13 @@ class MutableProxy(wrapt.ObjectProxy):
         Returns:
         Returns:
             The wrapped value.
             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.
         # Recursively wrap mutable types, but do not re-wrap MutableProxy instances.
         if self._is_mutable_type(value) and not isinstance(value, MutableProxy):
         if self._is_mutable_type(value) and not isinstance(value, MutableProxy):
-            return type(self)(
+            base_cls = globals()[self.__base_proxy__]
+            return base_cls(
                 wrapped=value,
                 wrapped=value,
                 state=self._self_state,
                 state=self._self_state,
                 field_name=self._self_field_name,
                 field_name=self._self_field_name,
@@ -3966,6 +4068,9 @@ class ImmutableMutableProxy(MutableProxy):
     to modify the wrapped object when the StateProxy is immutable.
     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(
     def _mark_dirty(
         self,
         self,
         wrapped=None,
         wrapped=None,

+ 3 - 3
reflex/testing.py

@@ -52,6 +52,7 @@ from reflex.state import (
     StateManagerRedis,
     StateManagerRedis,
     reload_state_module,
     reload_state_module,
 )
 )
+from reflex.utils import console
 
 
 try:
 try:
     from selenium import webdriver  # pyright: ignore [reportMissingImports]
     from selenium import webdriver  # pyright: ignore [reportMissingImports]
@@ -385,7 +386,7 @@ class AppHarness:
             )
             )
             if not line:
             if not line:
                 break
                 break
-            print(line)  # for pytest diagnosis
+            print(line)  # for pytest diagnosis #noqa: T201
             m = re.search(reflex.constants.Next.FRONTEND_LISTENING_REGEX, line)
             m = re.search(reflex.constants.Next.FRONTEND_LISTENING_REGEX, line)
             if m is not None:
             if m is not None:
                 self.frontend_url = m.group(1)
                 self.frontend_url = m.group(1)
@@ -403,11 +404,10 @@ class AppHarness:
                     )
                     )
                 # catch I/O operation on closed file.
                 # catch I/O operation on closed file.
                 except ValueError as e:
                 except ValueError as e:
-                    print(e)
+                    console.error(str(e))
                     break
                     break
                 if not line:
                 if not line:
                     break
                     break
-                print(line)
 
 
         self.frontend_output_thread = threading.Thread(target=consume_frontend_output)
         self.frontend_output_thread = threading.Thread(target=consume_frontend_output)
         self.frontend_output_thread.start()
         self.frontend_output_thread.start()

+ 4 - 0
reflex/utils/exceptions.py

@@ -187,3 +187,7 @@ def raise_system_package_missing_error(package: str) -> NoReturn:
 
 
 class InvalidLockWarningThresholdError(ReflexError):
 class InvalidLockWarningThresholdError(ReflexError):
     """Raised when an invalid lock warning threshold is provided."""
     """Raised when an invalid lock warning threshold is provided."""
+
+
+class UnretrievableVarValueError(ReflexError):
+    """Raised when the value of a var is not retrievable."""

+ 13 - 11
reflex/utils/prerequisites.py

@@ -28,8 +28,8 @@ import typer
 from alembic.util.exc import CommandError
 from alembic.util.exc import CommandError
 from packaging import version
 from packaging import version
 from redis import Redis as RedisSync
 from redis import Redis as RedisSync
-from redis import exceptions
 from redis.asyncio import Redis
 from redis.asyncio import Redis
+from redis.exceptions import RedisError
 
 
 from reflex import constants, model
 from reflex import constants, model
 from reflex.compiler import templates
 from reflex.compiler import templates
@@ -333,10 +333,11 @@ def get_redis() -> Redis | None:
     Returns:
     Returns:
         The asynchronous redis client.
         The asynchronous redis client.
     """
     """
-    if isinstance((redis_url_or_options := parse_redis_url()), str):
-        return Redis.from_url(redis_url_or_options)
-    elif isinstance(redis_url_or_options, dict):
-        return Redis(**redis_url_or_options)
+    if (redis_url := parse_redis_url()) is not None:
+        return Redis.from_url(
+            redis_url,
+            retry_on_error=[RedisError],
+        )
     return None
     return None
 
 
 
 
@@ -346,14 +347,15 @@ def get_redis_sync() -> RedisSync | None:
     Returns:
     Returns:
         The synchronous redis client.
         The synchronous redis client.
     """
     """
-    if isinstance((redis_url_or_options := parse_redis_url()), str):
-        return RedisSync.from_url(redis_url_or_options)
-    elif isinstance(redis_url_or_options, dict):
-        return RedisSync(**redis_url_or_options)
+    if (redis_url := parse_redis_url()) is not None:
+        return RedisSync.from_url(
+            redis_url,
+            retry_on_error=[RedisError],
+        )
     return None
     return None
 
 
 
 
-def parse_redis_url() -> str | dict | None:
+def parse_redis_url() -> str | None:
     """Parse the REDIS_URL in config if applicable.
     """Parse the REDIS_URL in config if applicable.
 
 
     Returns:
     Returns:
@@ -387,7 +389,7 @@ async def get_redis_status() -> dict[str, bool | None]:
             redis_client.ping()
             redis_client.ping()
         else:
         else:
             status = None
             status = None
-    except exceptions.RedisError:
+    except RedisError:
         status = False
         status = False
 
 
     return {"redis": status}
     return {"redis": status}

+ 1 - 1
reflex/utils/pyi_generator.py

@@ -1202,4 +1202,4 @@ class PyiGenerator:
                         or "Var[Template]" in line
                         or "Var[Template]" in line
                     ):
                     ):
                         line = line.rstrip() + "  # type: ignore\n"
                         line = line.rstrip() + "  # type: ignore\n"
-                    print(line, end="")
+                    print(line, end="")  # noqa: T201

+ 15 - 4
reflex/vars/base.py

@@ -182,7 +182,7 @@ class VarData:
         state: str = "",
         state: str = "",
         field_name: str = "",
         field_name: str = "",
         imports: ImportDict | ParsedImportDict | None = None,
         imports: ImportDict | ParsedImportDict | None = None,
-        hooks: dict[str, None] | None = None,
+        hooks: dict[str, VarData | None] | None = None,
         components: Iterable[BaseComponent] | None = None,
         components: Iterable[BaseComponent] | None = None,
         deps: list[Var] | None = None,
         deps: list[Var] | None = None,
         position: Hooks.HookPosition | None = None,
         position: Hooks.HookPosition | None = None,
@@ -254,7 +254,9 @@ class VarData:
             (var_data.state for var_data in all_var_datas if var_data.state), ""
             (var_data.state for var_data in all_var_datas if var_data.state), ""
         )
         )
 
 
-        hooks = {hook: None for var_data in all_var_datas for hook in var_data.hooks}
+        hooks: dict[str, VarData | None] = {
+            hook: None for var_data in all_var_datas for hook in var_data.hooks
+        }
 
 
         _imports = imports.merge_imports(
         _imports = imports.merge_imports(
             *(var_data.imports for var_data in all_var_datas)
             *(var_data.imports for var_data in all_var_datas)
@@ -616,7 +618,7 @@ class Var(Generic[VAR_TYPE]):
 
 
         # Try to pull the imports and hooks from contained values.
         # Try to pull the imports and hooks from contained values.
         if not isinstance(value, str):
         if not isinstance(value, str):
-            return LiteralVar.create(value)
+            return LiteralVar.create(value, _var_data=_var_data)
 
 
         if _var_is_string is False or _var_is_local is True:
         if _var_is_string is False or _var_is_local is True:
             return Var(
             return Var(
@@ -2469,7 +2471,7 @@ def computed_var(
 def computed_var(
 def computed_var(
     fget: Callable[[BASE_STATE], Any] | None = None,
     fget: Callable[[BASE_STATE], Any] | None = None,
     initial_value: Any | types.Unset = types.Unset(),
     initial_value: Any | types.Unset = types.Unset(),
-    cache: bool = False,
+    cache: Optional[bool] = None,
     deps: Optional[List[Union[str, Var]]] = None,
     deps: Optional[List[Union[str, Var]]] = None,
     auto_deps: bool = True,
     auto_deps: bool = True,
     interval: Optional[Union[datetime.timedelta, int]] = None,
     interval: Optional[Union[datetime.timedelta, int]] = None,
@@ -2495,6 +2497,15 @@ def computed_var(
         ValueError: If caching is disabled and an update interval is set.
         ValueError: If caching is disabled and an update interval is set.
         VarDependencyError: If user supplies dependencies without caching.
         VarDependencyError: If user supplies dependencies without caching.
     """
     """
+    if cache is None:
+        cache = False
+        console.deprecate(
+            "Default non-cached rx.var",
+            "the default value will be `@rx.var(cache=True)` in a future release. "
+            "To retain uncached var, explicitly pass `@rx.var(cache=False)`",
+            deprecation_version="0.6.8",
+            removal_version="0.7.0",
+        )
     if cache is False and interval is not None:
     if cache is False and interval is not None:
         raise ValueError("Cannot set update interval without caching.")
         raise ValueError("Cannot set update interval without caching.")
 
 

+ 16 - 0
reflex/vars/sequence.py

@@ -229,6 +229,22 @@ def string_starts_with_operation(full_string: Var[str], prefix: Var[str]):
     )
     )
 
 
 
 
+@var_operation
+def string_ends_with_operation(full_string: Var[str], suffix: Var[str]):
+    """Check if a string ends with a suffix.
+
+    Args:
+        full_string: The full string.
+        suffix: The suffix.
+
+    Returns:
+        Whether the string ends with the suffix.
+    """
+    return var_operation_return(
+        js_expression=f"{full_string}.endsWith({suffix})", var_type=bool
+    )
+
+
 @var_operation
 @var_operation
 def string_item_operation(string: Var[str], index: Var[int]):
 def string_item_operation(string: Var[str], index: Var[int]):
     """Get an item from a string.
     """Get an item from a string.

+ 3 - 3
scripts/wait_for_listening_port.py

@@ -25,7 +25,7 @@ def _pid_exists(pid):
 
 
 def _wait_for_port(port, server_pid, timeout) -> Tuple[bool, str]:
 def _wait_for_port(port, server_pid, timeout) -> Tuple[bool, str]:
     start = time.time()
     start = time.time()
-    print(f"Waiting for up to {timeout} seconds for port {port} to start listening.")
+    print(f"Waiting for up to {timeout} seconds for port {port} to start listening.")  # noqa: T201
     while True:
     while True:
         if not _pid_exists(server_pid):
         if not _pid_exists(server_pid):
             return False, f"Server PID {server_pid} is not running."
             return False, f"Server PID {server_pid} is not running."
@@ -56,9 +56,9 @@ def main():
     for f in as_completed(futures):
     for f in as_completed(futures):
         ok, msg = f.result()
         ok, msg = f.result()
         if ok:
         if ok:
-            print(f"OK: {msg}")
+            print(f"OK: {msg}")  # noqa: T201
         else:
         else:
-            print(f"FAIL: {msg}")
+            print(f"FAIL: {msg}")  # noqa: T201
             exit(1)
             exit(1)
 
 
 
 

+ 12 - 1
tests/integration/test_lifespan.py

@@ -43,6 +43,8 @@ def LifespanApp():
             lifespan_task_global = 0
             lifespan_task_global = 0
 
 
     class LifespanState(rx.State):
     class LifespanState(rx.State):
+        interval: int = 100
+
         @rx.var
         @rx.var
         def task_global(self) -> int:
         def task_global(self) -> int:
             return lifespan_task_global
             return lifespan_task_global
@@ -59,7 +61,15 @@ def LifespanApp():
         return rx.vstack(
         return rx.vstack(
             rx.text(LifespanState.task_global, id="task_global"),
             rx.text(LifespanState.task_global, id="task_global"),
             rx.text(LifespanState.context_global, id="context_global"),
             rx.text(LifespanState.context_global, id="context_global"),
-            rx.moment(interval=100, on_change=LifespanState.tick),
+            rx.button(
+                rx.moment(
+                    interval=LifespanState.interval, on_change=LifespanState.tick
+                ),
+                on_click=LifespanState.set_interval(  # type: ignore
+                    rx.cond(LifespanState.interval, 0, 100)
+                ),
+                id="toggle-tick",
+            ),
         )
         )
 
 
     app = rx.App()
     app = rx.App()
@@ -108,6 +118,7 @@ async def test_lifespan(lifespan_app: AppHarness):
     original_task_global_text = task_global.text
     original_task_global_text = task_global.text
     original_task_global_value = int(original_task_global_text)
     original_task_global_value = int(original_task_global_text)
     lifespan_app.poll_for_content(task_global, exp_not_equal=original_task_global_text)
     lifespan_app.poll_for_content(task_global, exp_not_equal=original_task_global_text)
+    driver.find_element(By.ID, "toggle-tick").click()  # avoid teardown errors
     assert lifespan_app.app_module.lifespan_task_global > original_task_global_value  # type: ignore
     assert lifespan_app.app_module.lifespan_task_global > original_task_global_value  # type: ignore
     assert int(task_global.text) > original_task_global_value
     assert int(task_global.text) > original_task_global_value
 
 

+ 129 - 31
tests/integration/test_upload.py

@@ -6,12 +6,16 @@ import asyncio
 import time
 import time
 from pathlib import Path
 from pathlib import Path
 from typing import Generator
 from typing import Generator
+from urllib.parse import urlsplit
 
 
 import pytest
 import pytest
 from selenium.webdriver.common.by import By
 from selenium.webdriver.common.by import By
 
 
+from reflex.constants.event import Endpoint
 from reflex.testing import AppHarness, WebDriver
 from reflex.testing import AppHarness, WebDriver
 
 
+from .utils import poll_for_navigation
+
 
 
 def UploadFile():
 def UploadFile():
     """App for testing dynamic routes."""
     """App for testing dynamic routes."""
@@ -23,7 +27,7 @@ def UploadFile():
 
 
     class UploadState(rx.State):
     class UploadState(rx.State):
         _file_data: Dict[str, str] = {}
         _file_data: Dict[str, str] = {}
-        event_order: List[str] = []
+        event_order: rx.Field[List[str]] = rx.field([])
         progress_dicts: List[dict] = []
         progress_dicts: List[dict] = []
         disabled: bool = False
         disabled: bool = False
         large_data: str = ""
         large_data: str = ""
@@ -50,6 +54,15 @@ def UploadFile():
             self.large_data = ""
             self.large_data = ""
             self.event_order.append("chain_event")
             self.event_order.append("chain_event")
 
 
+        async def handle_upload_tertiary(self, files: List[rx.UploadFile]):
+            for file in files:
+                (rx.get_upload_dir() / (file.filename or "INVALID")).write_bytes(
+                    await file.read()
+                )
+
+        def do_download(self):
+            return rx.download(rx.get_upload_url("test.txt"))
+
     def index():
     def index():
         return rx.vstack(
         return rx.vstack(
             rx.input(
             rx.input(
@@ -123,6 +136,34 @@ def UploadFile():
                 on_click=rx.cancel_upload("secondary"),
                 on_click=rx.cancel_upload("secondary"),
                 id="cancel_button_secondary",
                 id="cancel_button_secondary",
             ),
             ),
+            rx.heading("Tertiary Upload/Download"),
+            rx.upload.root(
+                rx.vstack(
+                    rx.button("Select File"),
+                    rx.text("Drag and drop files here or click to select files"),
+                ),
+                id="tertiary",
+            ),
+            rx.button(
+                "Upload",
+                on_click=UploadState.handle_upload_tertiary(  # type: ignore
+                    rx.upload_files(
+                        upload_id="tertiary",
+                    ),
+                ),
+                id="upload_button_tertiary",
+            ),
+            rx.button(
+                "Download - Frontend",
+                on_click=rx.download(rx.get_upload_url("test.txt")),
+                id="download-frontend",
+            ),
+            rx.button(
+                "Download - Backend",
+                on_click=UploadState.do_download,
+                id="download-backend",
+            ),
+            rx.text(UploadState.event_order.to_string(), id="event-order"),
         )
         )
 
 
     app = rx.App(state=rx.State)
     app = rx.App(state=rx.State)
@@ -164,6 +205,24 @@ def driver(upload_file: AppHarness):
         driver.quit()
         driver.quit()
 
 
 
 
+def poll_for_token(driver: WebDriver, upload_file: AppHarness) -> str:
+    """Poll for the token input to be populated.
+
+    Args:
+        driver: WebDriver instance.
+        upload_file: harness for UploadFile app.
+
+    Returns:
+        token value
+    """
+    token_input = driver.find_element(By.ID, "token")
+    assert token_input
+    # wait for the backend connection to send the token
+    token = upload_file.poll_for_value(token_input)
+    assert token is not None
+    return token
+
+
 @pytest.mark.parametrize("secondary", [False, True])
 @pytest.mark.parametrize("secondary", [False, True])
 @pytest.mark.asyncio
 @pytest.mark.asyncio
 async def test_upload_file(
 async def test_upload_file(
@@ -178,11 +237,7 @@ async def test_upload_file(
         secondary: whether to use the secondary upload form
         secondary: whether to use the secondary upload form
     """
     """
     assert upload_file.app_instance is not None
     assert upload_file.app_instance is not None
-    token_input = driver.find_element(By.ID, "token")
-    assert token_input
-    # wait for the backend connection to send the token
-    token = upload_file.poll_for_value(token_input)
-    assert token is not None
+    token = poll_for_token(driver, upload_file)
     full_state_name = upload_file.get_full_state_name(["_upload_state"])
     full_state_name = upload_file.get_full_state_name(["_upload_state"])
     state_name = upload_file.get_state_name("_upload_state")
     state_name = upload_file.get_state_name("_upload_state")
     substate_token = f"{token}_{full_state_name}"
     substate_token = f"{token}_{full_state_name}"
@@ -204,6 +259,19 @@ async def test_upload_file(
     upload_box.send_keys(str(target_file))
     upload_box.send_keys(str(target_file))
     upload_button.click()
     upload_button.click()
 
 
+    # check that the selected files are displayed
+    selected_files = driver.find_element(By.ID, f"selected_files{suffix}")
+    assert Path(selected_files.text).name == Path(exp_name).name
+
+    if secondary:
+        event_order_displayed = driver.find_element(By.ID, "event-order")
+        AppHarness._poll_for(lambda: "chain_event" in event_order_displayed.text)
+
+        state = await upload_file.get_state(substate_token)
+        # only the secondary form tracks progress and chain events
+        assert state.substates[state_name].event_order.count("upload_progress") == 1
+        assert state.substates[state_name].event_order.count("chain_event") == 1
+
     # look up the backend state and assert on uploaded contents
     # look up the backend state and assert on uploaded contents
     async def get_file_data():
     async def get_file_data():
         return (
         return (
@@ -217,16 +285,6 @@ async def test_upload_file(
     normalized_file_data = {Path(k).name: v for k, v in file_data.items()}
     normalized_file_data = {Path(k).name: v for k, v in file_data.items()}
     assert normalized_file_data[Path(exp_name).name] == exp_contents
     assert normalized_file_data[Path(exp_name).name] == exp_contents
 
 
-    # check that the selected files are displayed
-    selected_files = driver.find_element(By.ID, f"selected_files{suffix}")
-    assert Path(selected_files.text).name == Path(exp_name).name
-
-    state = await upload_file.get_state(substate_token)
-    if secondary:
-        # only the secondary form tracks progress and chain events
-        assert state.substates[state_name].event_order.count("upload_progress") == 1
-        assert state.substates[state_name].event_order.count("chain_event") == 1
-
 
 
 @pytest.mark.asyncio
 @pytest.mark.asyncio
 async def test_upload_file_multiple(tmp_path, upload_file: AppHarness, driver):
 async def test_upload_file_multiple(tmp_path, upload_file: AppHarness, driver):
@@ -238,11 +296,7 @@ async def test_upload_file_multiple(tmp_path, upload_file: AppHarness, driver):
         driver: WebDriver instance.
         driver: WebDriver instance.
     """
     """
     assert upload_file.app_instance is not None
     assert upload_file.app_instance is not None
-    token_input = driver.find_element(By.ID, "token")
-    assert token_input
-    # wait for the backend connection to send the token
-    token = upload_file.poll_for_value(token_input)
-    assert token is not None
+    token = poll_for_token(driver, upload_file)
     full_state_name = upload_file.get_full_state_name(["_upload_state"])
     full_state_name = upload_file.get_full_state_name(["_upload_state"])
     state_name = upload_file.get_state_name("_upload_state")
     state_name = upload_file.get_state_name("_upload_state")
     substate_token = f"{token}_{full_state_name}"
     substate_token = f"{token}_{full_state_name}"
@@ -301,11 +355,7 @@ def test_clear_files(
         secondary: whether to use the secondary upload form.
         secondary: whether to use the secondary upload form.
     """
     """
     assert upload_file.app_instance is not None
     assert upload_file.app_instance is not None
-    token_input = driver.find_element(By.ID, "token")
-    assert token_input
-    # wait for the backend connection to send the token
-    token = upload_file.poll_for_value(token_input)
-    assert token is not None
+    poll_for_token(driver, upload_file)
 
 
     suffix = "_secondary" if secondary else ""
     suffix = "_secondary" if secondary else ""
 
 
@@ -357,11 +407,7 @@ async def test_cancel_upload(tmp_path, upload_file: AppHarness, driver: WebDrive
         driver: WebDriver instance.
         driver: WebDriver instance.
     """
     """
     assert upload_file.app_instance is not None
     assert upload_file.app_instance is not None
-    token_input = driver.find_element(By.ID, "token")
-    assert token_input
-    # wait for the backend connection to send the token
-    token = upload_file.poll_for_value(token_input)
-    assert token is not None
+    token = poll_for_token(driver, upload_file)
     state_name = upload_file.get_state_name("_upload_state")
     state_name = upload_file.get_state_name("_upload_state")
     state_full_name = upload_file.get_full_state_name(["_upload_state"])
     state_full_name = upload_file.get_full_state_name(["_upload_state"])
     substate_token = f"{token}_{state_full_name}"
     substate_token = f"{token}_{state_full_name}"
@@ -403,3 +449,55 @@ async def test_cancel_upload(tmp_path, upload_file: AppHarness, driver: WebDrive
     assert Path(exp_name).name not in normalized_file_data
     assert Path(exp_name).name not in normalized_file_data
 
 
     target_file.unlink()
     target_file.unlink()
+
+
+@pytest.mark.asyncio
+async def test_upload_download_file(
+    tmp_path,
+    upload_file: AppHarness,
+    driver: WebDriver,
+):
+    """Submit a file upload and then fetch it with rx.download.
+
+    This checks the special case `getBackendURL` logic in the _download event
+    handler in state.js.
+
+    Args:
+        tmp_path: pytest tmp_path fixture
+        upload_file: harness for UploadFile app.
+        driver: WebDriver instance.
+    """
+    assert upload_file.app_instance is not None
+    poll_for_token(driver, upload_file)
+
+    upload_box = driver.find_elements(By.XPATH, "//input[@type='file']")[2]
+    assert upload_box
+    upload_button = driver.find_element(By.ID, "upload_button_tertiary")
+    assert upload_button
+
+    exp_name = "test.txt"
+    exp_contents = "test file contents!"
+    target_file = tmp_path / exp_name
+    target_file.write_text(exp_contents)
+
+    upload_box.send_keys(str(target_file))
+    upload_button.click()
+
+    # Download via event embedded in frontend code.
+    download_frontend = driver.find_element(By.ID, "download-frontend")
+    with poll_for_navigation(driver):
+        download_frontend.click()
+    assert urlsplit(driver.current_url).path == f"/{Endpoint.UPLOAD.value}/test.txt"
+    assert driver.find_element(by=By.TAG_NAME, value="body").text == exp_contents
+
+    # Go back and wait for the app to reload.
+    with poll_for_navigation(driver):
+        driver.back()
+    poll_for_token(driver, upload_file)
+
+    # Download via backend event handler.
+    download_backend = driver.find_element(By.ID, "download-backend")
+    with poll_for_navigation(driver):
+        download_backend.click()
+    assert urlsplit(driver.current_url).path == f"/{Endpoint.UPLOAD.value}/test.txt"
+    assert driver.find_element(by=By.TAG_NAME, value="body").text == exp_contents

+ 46 - 0
tests/integration/tests_playwright/test_link_hover.py

@@ -0,0 +1,46 @@
+from typing import Generator
+
+import pytest
+from playwright.sync_api import Page, expect
+
+from reflex.testing import AppHarness
+
+
+def LinkApp():
+    import reflex as rx
+
+    app = rx.App()
+
+    def index():
+        return rx.vstack(
+            rx.box(height="10em"),  # spacer, so the link isn't hovered initially
+            rx.link(
+                "Click me",
+                href="#",
+                color="blue",
+                _hover=rx.Style({"color": "red"}),
+            ),
+        )
+
+    app.add_page(index, "/")
+
+
+@pytest.fixture()
+def link_app(tmp_path_factory) -> Generator[AppHarness, None, None]:
+    with AppHarness.create(
+        root=tmp_path_factory.mktemp("link_app"),
+        app_source=LinkApp,  # type: ignore
+    ) as harness:
+        assert harness.app_instance is not None, "app is not running"
+        yield harness
+
+
+def test_link_hover(link_app: AppHarness, page: Page):
+    assert link_app.frontend_url is not None
+    page.goto(link_app.frontend_url)
+
+    link = page.get_by_role("link")
+    expect(link).to_have_text("Click me")
+    expect(link).to_have_css("color", "rgb(0, 0, 255)")
+    link.hover()
+    expect(link).to_have_css("color", "rgb(255, 0, 0)")

+ 8 - 2
tests/units/components/lucide/test_icon.py

@@ -1,13 +1,19 @@
 import pytest
 import pytest
 
 
-from reflex.components.lucide.icon import LUCIDE_ICON_LIST, Icon
+from reflex.components.lucide.icon import (
+    LUCIDE_ICON_LIST,
+    LUCIDE_ICON_MAPPING_OVERRIDE,
+    Icon,
+)
 from reflex.utils import format
 from reflex.utils import format
 
 
 
 
 @pytest.mark.parametrize("tag", LUCIDE_ICON_LIST)
 @pytest.mark.parametrize("tag", LUCIDE_ICON_LIST)
 def test_icon(tag):
 def test_icon(tag):
     icon = Icon.create(tag)
     icon = Icon.create(tag)
-    assert icon.alias == f"Lucide{format.to_title_case(tag)}Icon"
+    assert icon.alias == "Lucide" + LUCIDE_ICON_MAPPING_OVERRIDE.get(
+        tag, f"{format.to_title_case(tag)}Icon"
+    )
 
 
 
 
 def test_icon_missing_tag():
 def test_icon_missing_tag():

+ 14 - 4
tests/units/test_event.py

@@ -223,12 +223,17 @@ def test_event_console_log():
     )
     )
     assert (
     assert (
         format.format_event(spec)
         format.format_event(spec)
-        == 'Event("_call_function", {function:(() => (console["log"]("message")))})'
+        == 'Event("_call_function", {function:(() => (console["log"]("message"))),callback:null})'
     )
     )
     spec = event.console_log(Var(_js_expr="message"))
     spec = event.console_log(Var(_js_expr="message"))
     assert (
     assert (
         format.format_event(spec)
         format.format_event(spec)
-        == 'Event("_call_function", {function:(() => (console["log"](message)))})'
+        == 'Event("_call_function", {function:(() => (console["log"](message))),callback:null})'
+    )
+    spec2 = event.console_log(Var(_js_expr="message2")).add_args(Var("throwaway"))
+    assert (
+        format.format_event(spec2)
+        == 'Event("_call_function", {function:(() => (console["log"](message2))),callback:null})'
     )
     )
 
 
 
 
@@ -243,12 +248,17 @@ def test_event_window_alert():
     )
     )
     assert (
     assert (
         format.format_event(spec)
         format.format_event(spec)
-        == 'Event("_call_function", {function:(() => (window["alert"]("message")))})'
+        == 'Event("_call_function", {function:(() => (window["alert"]("message"))),callback:null})'
     )
     )
     spec = event.window_alert(Var(_js_expr="message"))
     spec = event.window_alert(Var(_js_expr="message"))
     assert (
     assert (
         format.format_event(spec)
         format.format_event(spec)
-        == 'Event("_call_function", {function:(() => (window["alert"](message)))})'
+        == 'Event("_call_function", {function:(() => (window["alert"](message))),callback:null})'
+    )
+    spec2 = event.window_alert(Var(_js_expr="message2")).add_args(Var("throwaway"))
+    assert (
+        format.format_event(spec2)
+        == 'Event("_call_function", {function:(() => (window["alert"](message2))),callback:null})'
     )
     )
 
 
 
 

+ 66 - 15
tests/units/test_state.py

@@ -60,6 +60,7 @@ from reflex.utils.exceptions import (
     ReflexRuntimeError,
     ReflexRuntimeError,
     SetUndefinedStateVarError,
     SetUndefinedStateVarError,
     StateSerializationError,
     StateSerializationError,
+    UnretrievableVarValueError,
 )
 )
 from reflex.utils.format import json_dumps
 from reflex.utils.format import json_dumps
 from reflex.vars.base import Var, computed_var
 from reflex.vars.base import Var, computed_var
@@ -115,7 +116,7 @@ class TestState(BaseState):
     # Set this class as not test one
     # Set this class as not test one
     __test__ = False
     __test__ = False
 
 
-    num1: int
+    num1: rx.Field[int]
     num2: float = 3.14
     num2: float = 3.14
     key: str
     key: str
     map_key: str = "a"
     map_key: str = "a"
@@ -163,7 +164,7 @@ class ChildState(TestState):
     """A child state fixture."""
     """A child state fixture."""
 
 
     value: str
     value: str
-    count: int = 23
+    count: rx.Field[int] = rx.field(23)
 
 
     def change_both(self, value: str, count: int):
     def change_both(self, value: str, count: int):
         """Change both the value and count.
         """Change both the value and count.
@@ -1663,7 +1664,7 @@ async def state_manager(request) -> AsyncGenerator[StateManager, None]:
 
 
 
 
 @pytest.fixture()
 @pytest.fixture()
-def substate_token(state_manager, token):
+def substate_token(state_manager, token) -> str:
     """A token + substate name for looking up in state manager.
     """A token + substate name for looking up in state manager.
 
 
     Args:
     Args:
@@ -1936,6 +1937,14 @@ def mock_app(mock_app_simple: rx.App, state_manager: StateManager) -> rx.App:
     return mock_app_simple
     return mock_app_simple
 
 
 
 
+@dataclasses.dataclass
+class ModelDC:
+    """A dataclass."""
+
+    foo: str = "bar"
+    ls: list[dict] = dataclasses.field(default_factory=list)
+
+
 @pytest.mark.asyncio
 @pytest.mark.asyncio
 async def test_state_proxy(grandchild_state: GrandchildState, mock_app: rx.App):
 async def test_state_proxy(grandchild_state: GrandchildState, mock_app: rx.App):
     """Test that the state proxy works.
     """Test that the state proxy works.
@@ -2038,6 +2047,7 @@ class BackgroundTaskState(BaseState):
 
 
     order: List[str] = []
     order: List[str] = []
     dict_list: Dict[str, List[int]] = {"foo": [1, 2, 3]}
     dict_list: Dict[str, List[int]] = {"foo": [1, 2, 3]}
+    dc: ModelDC = ModelDC()
 
 
     def __init__(self, **kwargs):  # noqa: D107
     def __init__(self, **kwargs):  # noqa: D107
         super().__init__(**kwargs)
         super().__init__(**kwargs)
@@ -2063,10 +2073,18 @@ class BackgroundTaskState(BaseState):
         with pytest.raises(ImmutableStateError):
         with pytest.raises(ImmutableStateError):
             self.order.append("bad idea")
             self.order.append("bad idea")
 
 
+        with pytest.raises(ImmutableStateError):
+            # Cannot manipulate dataclass attributes.
+            self.dc.foo = "baz"
+
         with pytest.raises(ImmutableStateError):
         with pytest.raises(ImmutableStateError):
             # Even nested access to mutables raises an exception.
             # Even nested access to mutables raises an exception.
             self.dict_list["foo"].append(42)
             self.dict_list["foo"].append(42)
 
 
+        with pytest.raises(ImmutableStateError):
+            # Cannot modify dataclass list attribute.
+            self.dc.ls.append({"foo": "bar"})
+
         with pytest.raises(ImmutableStateError):
         with pytest.raises(ImmutableStateError):
             # Direct calling another handler that modifies state raises an exception.
             # Direct calling another handler that modifies state raises an exception.
             self.other()
             self.other()
@@ -3582,13 +3600,6 @@ class ModelV2(BaseModelV2):
     foo: str = "bar"
     foo: str = "bar"
 
 
 
 
-@dataclasses.dataclass
-class ModelDC:
-    """A dataclass."""
-
-    foo: str = "bar"
-
-
 class PydanticState(rx.State):
 class PydanticState(rx.State):
     """A state with pydantic BaseModel vars."""
     """A state with pydantic BaseModel vars."""
 
 
@@ -3610,11 +3621,22 @@ def test_mutable_models():
     assert state.dirty_vars == {"v2"}
     assert state.dirty_vars == {"v2"}
     state.dirty_vars.clear()
     state.dirty_vars.clear()
 
 
-    # Not yet supported ENG-4083
-    # assert isinstance(state.dc, MutableProxy) #noqa: ERA001
-    # state.dc.foo = "baz" #noqa: ERA001
-    # assert state.dirty_vars == {"dc"} #noqa: ERA001
-    # state.dirty_vars.clear() #noqa: ERA001
+    assert isinstance(state.dc, MutableProxy)
+    state.dc.foo = "baz"
+    assert state.dirty_vars == {"dc"}
+    state.dirty_vars.clear()
+    assert state.dirty_vars == set()
+    state.dc.ls.append({"hi": "reflex"})
+    assert state.dirty_vars == {"dc"}
+    state.dirty_vars.clear()
+    assert state.dirty_vars == set()
+    assert dataclasses.asdict(state.dc) == {"foo": "baz", "ls": [{"hi": "reflex"}]}
+    assert dataclasses.astuple(state.dc) == ("baz", [{"hi": "reflex"}])
+    # creating a new instance shouldn't mark the state dirty
+    assert dataclasses.replace(state.dc, foo="quuc") == ModelDC(
+        foo="quuc", ls=[{"hi": "reflex"}]
+    )
+    assert state.dirty_vars == set()
 
 
 
 
 def test_get_value():
 def test_get_value():
@@ -3764,3 +3786,32 @@ async def test_upcast_event_handler_arg(handler, payload):
     state = UpcastState()
     state = UpcastState()
     async for update in state._process_event(handler, state, payload):
     async for update in state._process_event(handler, state, payload):
         assert update.delta == {UpcastState.get_full_name(): {"passed": True}}
         assert update.delta == {UpcastState.get_full_name(): {"passed": True}}
+
+
+@pytest.mark.asyncio
+async def test_get_var_value(state_manager: StateManager, substate_token: str):
+    """Test that get_var_value works correctly.
+
+    Args:
+        state_manager: The state manager to use.
+        substate_token: Token for the substate used by state_manager.
+    """
+    state = await state_manager.get_state(substate_token)
+
+    # State Var from same state
+    assert await state.get_var_value(TestState.num1) == 0
+    state.num1 = 42
+    assert await state.get_var_value(TestState.num1) == 42
+
+    # State Var from another state
+    child_state = await state.get_state(ChildState)
+    assert await state.get_var_value(ChildState.count) == 23
+    child_state.count = 66
+    assert await state.get_var_value(ChildState.count) == 66
+
+    # LiteralVar with known value
+    assert await state.get_var_value(rx.Var.create([1, 2, 3])) == [1, 2, 3]
+
+    # Generic Var with no state
+    with pytest.raises(UnretrievableVarValueError):
+        await state.get_var_value(rx.Var("undefined"))