Преглед на файлове

delay page until _compile gets called (#3812)

* basic functionality

* reorder page evaluation

* Fix benchmark tests (#3822)

* Upper bound for reflex-chakra dependency (#3824)

* Upper bound for reflex-chakra dependency

We have to make a breaking change in reflex-chakra, and it will happen in 0.6.0, so ensure that reflex 0.5.10 never installs the breaking version of reflex-chakra.

* Relock deps

* Add comments to DataList components (#3827)

* fix: Adding missing comments to the data list

* fix: Ran make pyi

* fully migrate vars into new system (#3743)

* fully migrate vars into new system

* i hate rufffff (no i don't)

* fix silly pright issues (except colormode and state)

* remove all instances of Var.create

* create immutable callable var and get rid of more base vars

* implement hash for all functions

* get reflex-web to compile

* get it to compile reflex-web successfully

* fix tests

* fix pyi

* use override from typing_extension

* put plotly inside of a catch

* dicts are unusable sadly

* fix silly mistake

* overload equals to special case immutable var

* improve test_cond

* solve more CI issues, down to 94 failures

* down to 20 errors

* down to 13 errors

* pass all testcases

* fix pyright issues

* reorder things

* use get origin more

* use fixed_type logic

* various optimizations

* go back to passing test cases

* use less boilerplate

* remove unnecessary print message

* remove weird comment

* add test for html issue

* add type ignore

* fix another silly issue

* override get all var data for var operations call

* make integration tests pass

* fix immutable call var

* better logic for finding parent class

* use even better logic for finding state wrt computedvar

* only choose the ones that are defined in the same module

* small dict to large dict

* [REF-3591] Remove chakra-related files from immutable vars PR (#3821)

* Add comments to html metadata component (#3731)

* fix: add verification for path /404 (#3723)

Co-authored-by: coolstorm <manas.gupta@fampay.in>

* Use the new state name when setting `is_hydrated` to false (#3738)

* Use `._is_mutable()` to account for parent state proxy (#3739)

When a parent state proxy is set, also allow child StateProxy._self_mutable to
override the parent's `_is_mutable()`.

* bump to 0.5.9 (#3746)

* add message when installing requirements.txt is needed for chosen template during init (#3750)

* #3752 bugfix add domain for XAxis (#3764)

* fix appharness app_source typing (#3777)

* fix import clash between connectionToaster and hooks.useState (#3749)

* use different registry when in china, fixes #3700 (#3702)

* do not reload compilation if using local app in AppHarness (#3790)

* do not reload if using local app

* Update reflex/testing.py

Co-authored-by: Masen Furer <m_github@0x26.net>

---------

Co-authored-by: Masen Furer <m_github@0x26.net>

* Bump memory on relevant actions (#3781)

Co-authored-by: Alek Petuskey <alekpetuskey@Aleks-MacBook-Pro.local>

* [REF-3334] Validate Toast Props (#3793)

* [REF-3536][REF-3537][REF-3541] Move chakra components into its repo(reflex-chakra) (#3798)

* fix get_uuid_string_var (#3795)

* minor State cleanup (#3768)

* Fix code wrap in markdown (#3755)

---------

Co-authored-by: Alek Petuskey <alek@pynecone.io>
Co-authored-by: Manas Gupta <53006261+Manas1820@users.noreply.github.com>
Co-authored-by: coolstorm <manas.gupta@fampay.in>
Co-authored-by: Thomas Brandého <thomas.brandeho@gmail.com>
Co-authored-by: Shubhankar Dimri <dimrishubhi@gmail.com>
Co-authored-by: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com>
Co-authored-by: Khaleel Al-Adhami <khaleel.aladhami@gmail.com>
Co-authored-by: Alek Petuskey <alekpetuskey@Aleks-MacBook-Pro.local>
Co-authored-by: Elijah Ahianyo <elijahahianyo@gmail.com>

* pyproject.toml: bump to 0.6.0a1

* pyproject.toml: depend on reflex-chakra>=0.6.0a

New Var system support in reflex-chakra 0.6.0a1

* poetry.lock: relock dependencies

* integration: bump listening timeout to 1200 seconds

* integration: bump listening timeout to 1800 seconds

* Use cached_var_no_lock to avoid ImmutableVar deadlocks (#3835)

* Use cached_var_no_lock to avoid ImmutableVar deadlocks

ImmutableVar subclasses will always return the same value for a _var_name or
_get_all_var_data so there is no need to use a per-class lock to protect a
cached attribute on an instance, and doing so actually is observed to cause
deadlocks when a particular _cached_var_name creates new LiteralVar instances
and attempts to serialize them.

* remove unused module global

---------

Co-authored-by: Masen Furer <m_github@0x26.net>
Co-authored-by: Alek Petuskey <alek@pynecone.io>
Co-authored-by: Manas Gupta <53006261+Manas1820@users.noreply.github.com>
Co-authored-by: coolstorm <manas.gupta@fampay.in>
Co-authored-by: Thomas Brandého <thomas.brandeho@gmail.com>
Co-authored-by: Shubhankar Dimri <dimrishubhi@gmail.com>
Co-authored-by: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com>
Co-authored-by: Alek Petuskey <alekpetuskey@Aleks-MacBook-Pro.local>
Co-authored-by: Elijah Ahianyo <elijahahianyo@gmail.com>

* guess_type: if the type is optional, treat it like it's "not None" (#3839)

* guess_type: if the type is optional, treat it like it's "not None"

When guessing the type for an Optional annotation, the Optional was already
being stripped off, but this value was being ignored, except for error
messages. So actually use the Optional-stripped value.

* Strip Optional when casting Var .to

* Fix double-quoting of defaultColorMode (#3840)

If the user provided an `appearance` or `color_mode` value to `rx.theme`, then
the resulting value ended up being double-double-quoted in the resulting JS
output.

Instead remove the quotes from the context.js.jinja2 and always pass appearance
as a Var.

* basic functionality

* run pyright

* fully migrate vars into new system (#3743)

* fully migrate vars into new system

* i hate rufffff (no i don't)

* fix silly pright issues (except colormode and state)

* remove all instances of Var.create

* create immutable callable var and get rid of more base vars

* implement hash for all functions

* get reflex-web to compile

* get it to compile reflex-web successfully

* fix tests

* fix pyi

* use override from typing_extension

* put plotly inside of a catch

* dicts are unusable sadly

* fix silly mistake

* overload equals to special case immutable var

* improve test_cond

* solve more CI issues, down to 94 failures

* down to 20 errors

* down to 13 errors

* pass all testcases

* fix pyright issues

* reorder things

* use get origin more

* use fixed_type logic

* various optimizations

* go back to passing test cases

* use less boilerplate

* remove unnecessary print message

* remove weird comment

* add test for html issue

* add type ignore

* fix another silly issue

* override get all var data for var operations call

* make integration tests pass

* fix immutable call var

* better logic for finding parent class

* use even better logic for finding state wrt computedvar

* only choose the ones that are defined in the same module

* small dict to large dict

* [REF-3591] Remove chakra-related files from immutable vars PR (#3821)

* Add comments to html metadata component (#3731)

* fix: add verification for path /404 (#3723)

Co-authored-by: coolstorm <manas.gupta@fampay.in>

* Use the new state name when setting `is_hydrated` to false (#3738)

* Use `._is_mutable()` to account for parent state proxy (#3739)

When a parent state proxy is set, also allow child StateProxy._self_mutable to
override the parent's `_is_mutable()`.

* bump to 0.5.9 (#3746)

* add message when installing requirements.txt is needed for chosen template during init (#3750)

* #3752 bugfix add domain for XAxis (#3764)

* fix appharness app_source typing (#3777)

* fix import clash between connectionToaster and hooks.useState (#3749)

* use different registry when in china, fixes #3700 (#3702)

* do not reload compilation if using local app in AppHarness (#3790)

* do not reload if using local app

* Update reflex/testing.py

Co-authored-by: Masen Furer <m_github@0x26.net>

---------

Co-authored-by: Masen Furer <m_github@0x26.net>

* Bump memory on relevant actions (#3781)

Co-authored-by: Alek Petuskey <alekpetuskey@Aleks-MacBook-Pro.local>

* [REF-3334] Validate Toast Props (#3793)

* [REF-3536][REF-3537][REF-3541] Move chakra components into its repo(reflex-chakra) (#3798)

* fix get_uuid_string_var (#3795)

* minor State cleanup (#3768)

* Fix code wrap in markdown (#3755)

---------

Co-authored-by: Alek Petuskey <alek@pynecone.io>
Co-authored-by: Manas Gupta <53006261+Manas1820@users.noreply.github.com>
Co-authored-by: coolstorm <manas.gupta@fampay.in>
Co-authored-by: Thomas Brandého <thomas.brandeho@gmail.com>
Co-authored-by: Shubhankar Dimri <dimrishubhi@gmail.com>
Co-authored-by: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com>
Co-authored-by: Khaleel Al-Adhami <khaleel.aladhami@gmail.com>
Co-authored-by: Alek Petuskey <alekpetuskey@Aleks-MacBook-Pro.local>
Co-authored-by: Elijah Ahianyo <elijahahianyo@gmail.com>

* pyproject.toml: bump to 0.6.0a1

* pyproject.toml: depend on reflex-chakra>=0.6.0a

New Var system support in reflex-chakra 0.6.0a1

* poetry.lock: relock dependencies

* integration: bump listening timeout to 1200 seconds

* integration: bump listening timeout to 1800 seconds

* Use cached_var_no_lock to avoid ImmutableVar deadlocks (#3835)

* Use cached_var_no_lock to avoid ImmutableVar deadlocks

ImmutableVar subclasses will always return the same value for a _var_name or
_get_all_var_data so there is no need to use a per-class lock to protect a
cached attribute on an instance, and doing so actually is observed to cause
deadlocks when a particular _cached_var_name creates new LiteralVar instances
and attempts to serialize them.

* remove unused module global

---------

Co-authored-by: Masen Furer <m_github@0x26.net>
Co-authored-by: Alek Petuskey <alek@pynecone.io>
Co-authored-by: Manas Gupta <53006261+Manas1820@users.noreply.github.com>
Co-authored-by: coolstorm <manas.gupta@fampay.in>
Co-authored-by: Thomas Brandého <thomas.brandeho@gmail.com>
Co-authored-by: Shubhankar Dimri <dimrishubhi@gmail.com>
Co-authored-by: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com>
Co-authored-by: Alek Petuskey <alekpetuskey@Aleks-MacBook-Pro.local>
Co-authored-by: Elijah Ahianyo <elijahahianyo@gmail.com>

* basic functionality

* reorder page evaluation

* fully migrate vars into new system (#3743)

* fully migrate vars into new system

* i hate rufffff (no i don't)

* fix silly pright issues (except colormode and state)

* remove all instances of Var.create

* create immutable callable var and get rid of more base vars

* implement hash for all functions

* get reflex-web to compile

* get it to compile reflex-web successfully

* fix tests

* fix pyi

* use override from typing_extension

* put plotly inside of a catch

* dicts are unusable sadly

* fix silly mistake

* overload equals to special case immutable var

* improve test_cond

* solve more CI issues, down to 94 failures

* down to 20 errors

* down to 13 errors

* pass all testcases

* fix pyright issues

* reorder things

* use get origin more

* use fixed_type logic

* various optimizations

* go back to passing test cases

* use less boilerplate

* remove unnecessary print message

* remove weird comment

* add test for html issue

* add type ignore

* fix another silly issue

* override get all var data for var operations call

* make integration tests pass

* fix immutable call var

* better logic for finding parent class

* use even better logic for finding state wrt computedvar

* only choose the ones that are defined in the same module

* small dict to large dict

* [REF-3591] Remove chakra-related files from immutable vars PR (#3821)

* Add comments to html metadata component (#3731)

* fix: add verification for path /404 (#3723)

Co-authored-by: coolstorm <manas.gupta@fampay.in>

* Use the new state name when setting `is_hydrated` to false (#3738)

* Use `._is_mutable()` to account for parent state proxy (#3739)

When a parent state proxy is set, also allow child StateProxy._self_mutable to
override the parent's `_is_mutable()`.

* bump to 0.5.9 (#3746)

* add message when installing requirements.txt is needed for chosen template during init (#3750)

* #3752 bugfix add domain for XAxis (#3764)

* fix appharness app_source typing (#3777)

* fix import clash between connectionToaster and hooks.useState (#3749)

* use different registry when in china, fixes #3700 (#3702)

* do not reload compilation if using local app in AppHarness (#3790)

* do not reload if using local app

* Update reflex/testing.py

Co-authored-by: Masen Furer <m_github@0x26.net>

---------

Co-authored-by: Masen Furer <m_github@0x26.net>

* Bump memory on relevant actions (#3781)

Co-authored-by: Alek Petuskey <alekpetuskey@Aleks-MacBook-Pro.local>

* [REF-3334] Validate Toast Props (#3793)

* [REF-3536][REF-3537][REF-3541] Move chakra components into its repo(reflex-chakra) (#3798)

* fix get_uuid_string_var (#3795)

* minor State cleanup (#3768)

* Fix code wrap in markdown (#3755)

---------

Co-authored-by: Alek Petuskey <alek@pynecone.io>
Co-authored-by: Manas Gupta <53006261+Manas1820@users.noreply.github.com>
Co-authored-by: coolstorm <manas.gupta@fampay.in>
Co-authored-by: Thomas Brandého <thomas.brandeho@gmail.com>
Co-authored-by: Shubhankar Dimri <dimrishubhi@gmail.com>
Co-authored-by: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com>
Co-authored-by: Khaleel Al-Adhami <khaleel.aladhami@gmail.com>
Co-authored-by: Alek Petuskey <alekpetuskey@Aleks-MacBook-Pro.local>
Co-authored-by: Elijah Ahianyo <elijahahianyo@gmail.com>

* pyproject.toml: bump to 0.6.0a1

* pyproject.toml: depend on reflex-chakra>=0.6.0a

New Var system support in reflex-chakra 0.6.0a1

* poetry.lock: relock dependencies

* integration: bump listening timeout to 1200 seconds

* integration: bump listening timeout to 1800 seconds

* Use cached_var_no_lock to avoid ImmutableVar deadlocks (#3835)

* Use cached_var_no_lock to avoid ImmutableVar deadlocks

ImmutableVar subclasses will always return the same value for a _var_name or
_get_all_var_data so there is no need to use a per-class lock to protect a
cached attribute on an instance, and doing so actually is observed to cause
deadlocks when a particular _cached_var_name creates new LiteralVar instances
and attempts to serialize them.

* remove unused module global

---------

Co-authored-by: Masen Furer <m_github@0x26.net>
Co-authored-by: Alek Petuskey <alek@pynecone.io>
Co-authored-by: Manas Gupta <53006261+Manas1820@users.noreply.github.com>
Co-authored-by: coolstorm <manas.gupta@fampay.in>
Co-authored-by: Thomas Brandého <thomas.brandeho@gmail.com>
Co-authored-by: Shubhankar Dimri <dimrishubhi@gmail.com>
Co-authored-by: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com>
Co-authored-by: Alek Petuskey <alekpetuskey@Aleks-MacBook-Pro.local>
Co-authored-by: Elijah Ahianyo <elijahahianyo@gmail.com>

* guess_type: if the type is optional, treat it like it's "not None" (#3839)

* guess_type: if the type is optional, treat it like it's "not None"

When guessing the type for an Optional annotation, the Optional was already
being stripped off, but this value was being ignored, except for error
messages. So actually use the Optional-stripped value.

* Strip Optional when casting Var .to

* run format

* use original mp

* evaluate page regardless

* use old typing

* dangit pydantic

* i have two braincells and they are NOT collaborating

* adjust testcases

* always add the upload endpoint

* retrieve upload but after evaluate component

* check against none

Co-authored-by: Masen Furer <m_github@0x26.net>

* make it var

* fix page title

* don't change style.py

* fix counter

* remove duplicated logic

---------

Co-authored-by: Elijah Ahianyo <elijahahianyo@gmail.com>
Co-authored-by: Masen Furer <m_github@0x26.net>
Co-authored-by: elvis kahoro <elvis@reflex.dev>
Co-authored-by: Alek Petuskey <alek@pynecone.io>
Co-authored-by: Manas Gupta <53006261+Manas1820@users.noreply.github.com>
Co-authored-by: coolstorm <manas.gupta@fampay.in>
Co-authored-by: Thomas Brandého <thomas.brandeho@gmail.com>
Co-authored-by: Shubhankar Dimri <dimrishubhi@gmail.com>
Co-authored-by: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com>
Co-authored-by: Alek Petuskey <alekpetuskey@Aleks-MacBook-Pro.local>
Khaleel Al-Adhami преди 6 месеца
родител
ревизия
788b21556d
променени са 3 файла, в които са добавени 273 реда и са изтрити 164 реда
  1. 155 125
      reflex/app.py
  2. 101 35
      reflex/compiler/compiler.py
  3. 17 4
      tests/units/test_app.py

+ 155 - 125
reflex/app.py

@@ -6,6 +6,7 @@ import asyncio
 import concurrent.futures
 import contextlib
 import copy
+import dataclasses
 import functools
 import inspect
 import io
@@ -18,6 +19,7 @@ import traceback
 from datetime import datetime
 from pathlib import Path
 from typing import (
+    TYPE_CHECKING,
     Any,
     AsyncIterator,
     Callable,
@@ -47,7 +49,10 @@ from reflex.app_mixins import AppMixin, LifespanMixin, MiddlewareMixin
 from reflex.base import Base
 from reflex.compiler import compiler
 from reflex.compiler import utils as compiler_utils
-from reflex.compiler.compiler import ExecutorSafeFunctions
+from reflex.compiler.compiler import (
+    ExecutorSafeFunctions,
+    compile_theme,
+)
 from reflex.components.base.app_wrap import AppWrap
 from reflex.components.base.error_boundary import ErrorBoundary
 from reflex.components.base.fragment import Fragment
@@ -88,6 +93,9 @@ from reflex.utils import codespaces, console, exceptions, format, prerequisites,
 from reflex.utils.exec import is_prod_mode, is_testing_env, should_skip_compile
 from reflex.utils.imports import ImportVar
 
+if TYPE_CHECKING:
+    from reflex.vars import Var
+
 # Define custom types.
 ComponentCallable = Callable[[], Component]
 Reducer = Callable[[Event], Coroutine[Any, Any, StateUpdate]]
@@ -170,6 +178,21 @@ class OverlayFragment(Fragment):
     pass
 
 
+@dataclasses.dataclass(
+    frozen=True,
+)
+class UnevaluatedPage:
+    """An uncompiled page."""
+
+    component: Union[Component, ComponentCallable]
+    route: str
+    title: Union[Var, str, None]
+    description: Union[Var, str, None]
+    image: str
+    on_load: Union[EventHandler, EventSpec, List[Union[EventHandler, EventSpec]], None]
+    meta: List[Dict[str, str]]
+
+
 class App(MiddlewareMixin, LifespanMixin, Base):
     """The main Reflex app that encapsulates the backend and frontend.
 
@@ -220,6 +243,9 @@ class App(MiddlewareMixin, LifespanMixin, Base):
     # Attributes to add to the html root tag of every page.
     html_custom_attrs: Optional[Dict[str, str]] = None
 
+    # A map from a route to an unevaluated page. PRIVATE.
+    unevaluated_pages: Dict[str, UnevaluatedPage] = {}
+
     # A map from a page route to the component to render. Users should use `add_page`. PRIVATE.
     pages: Dict[str, Component] = {}
 
@@ -381,8 +407,8 @@ class App(MiddlewareMixin, LifespanMixin, Base):
 
     def _add_optional_endpoints(self):
         """Add optional api endpoints (_upload)."""
-        # To upload files.
         if Upload.is_used:
+            # To upload files.
             self.api.post(str(constants.Endpoint.UPLOAD))(upload(self))
 
             # To access uploaded files.
@@ -442,8 +468,8 @@ class App(MiddlewareMixin, LifespanMixin, Base):
         self,
         component: Component | ComponentCallable,
         route: str | None = None,
-        title: str | None = None,
-        description: str | None = None,
+        title: str | Var | None = None,
+        description: str | Var | None = None,
         image: str = constants.DefaultPage.IMAGE,
         on_load: (
             EventHandler | EventSpec | list[EventHandler | EventSpec] | None
@@ -479,13 +505,13 @@ class App(MiddlewareMixin, LifespanMixin, Base):
         # Check if the route given is valid
         verify_route_validity(route)
 
-        if route in self.pages and os.getenv(constants.RELOAD_CONFIG):
+        if route in self.unevaluated_pages and os.getenv(constants.RELOAD_CONFIG):
             # when the app is reloaded(typically for app harness tests), we should maintain
             # the latest render function of a route.This applies typically to decorated pages
             # since they are only added when app._compile is called.
-            self.pages.pop(route)
+            self.unevaluated_pages.pop(route)
 
-        if route in self.pages:
+        if route in self.unevaluated_pages:
             route_name = (
                 f"`{route}` or `/`"
                 if route == constants.PageNames.INDEX_ROUTE
@@ -501,58 +527,38 @@ class App(MiddlewareMixin, LifespanMixin, Base):
         state = self.state if self.state else State
         state.setup_dynamic_args(get_route_args(route))
 
-        # Generate the component if it is a callable.
-        component = self._generate_component(component)
-
-        # unpack components that return tuples in an rx.fragment.
-        if isinstance(component, tuple):
-            component = Fragment.create(*component)
-
-        # Ensure state is enabled if this page uses state.
-        if self.state is None:
-            if on_load or component._has_stateful_event_triggers():
-                self._enable_state()
-            else:
-                for var in component._get_vars(include_children=True):
-                    var_data = var._get_all_var_data()
-                    if not var_data:
-                        continue
-                    if not var_data.state:
-                        continue
-                    self._enable_state()
-                    break
-
-        component = OverlayFragment.create(component)
+        if on_load:
+            self.load_events[route] = (
+                on_load if isinstance(on_load, list) else [on_load]
+            )
 
-        meta_args = {
-            "title": (
-                title
-                if title is not None
-                else format.make_default_page_title(get_config().app_name, route)
-            ),
-            "image": image,
-            "meta": meta,
-        }
+        self.unevaluated_pages[route] = UnevaluatedPage(
+            component=component,
+            route=route,
+            title=title,
+            description=description,
+            image=image,
+            on_load=on_load,
+            meta=meta,
+        )
 
-        if description is not None:
-            meta_args["description"] = description
+    def _compile_page(self, route: str):
+        """Compile a page.
 
-        # Add meta information to the component.
-        compiler_utils.add_meta(
-            component,
-            **meta_args,
+        Args:
+            route: The route of the page to compile.
+        """
+        component, enable_state = compiler.compile_unevaluated_page(
+            route, self.unevaluated_pages[route], self.state
         )
 
+        if enable_state:
+            self._enable_state()
+
         # Add the page.
         self._check_routes_conflict(route)
         self.pages[route] = component
 
-        # Add the load events.
-        if on_load:
-            if not isinstance(on_load, list):
-                on_load = [on_load]
-            self.load_events[route] = on_load
-
     def get_load_events(self, route: str) -> list[EventHandler | EventSpec]:
         """Get the load events for a route.
 
@@ -827,13 +833,18 @@ class App(MiddlewareMixin, LifespanMixin, Base):
         """
         from reflex.utils.exceptions import ReflexRuntimeError
 
+        self.pages = {}
+
         def get_compilation_time() -> str:
             return str(datetime.now().time()).split(".")[0]
 
         # Render a default 404 page if the user didn't supply one
-        if constants.Page404.SLUG not in self.pages:
+        if constants.Page404.SLUG not in self.unevaluated_pages:
             self.add_custom_404_page()
 
+        for route in self.unevaluated_pages:
+            self._compile_page(route)
+
         # Add the optional endpoints (_upload)
         self._add_optional_endpoints()
 
@@ -857,7 +868,7 @@ class App(MiddlewareMixin, LifespanMixin, Base):
         progress.start()
         task = progress.add_task(
             f"[{get_compilation_time()}] Compiling:",
-            total=len(self.pages)
+            total=len(self.unevaluated_pages)
             + fixed_pages_within_executor
             + adhoc_steps_without_executor,
         )
@@ -886,38 +897,8 @@ class App(MiddlewareMixin, LifespanMixin, Base):
         all_imports = {}
         custom_components = set()
 
-        for _route, component in self.pages.items():
-            # Merge the component style with the app style.
-            component._add_style_recursive(self.style, self.theme)
-
-            # Add component._get_all_imports() to all_imports.
-            all_imports.update(component._get_all_imports())
-
-            # Add the app wrappers from this component.
-            app_wrappers.update(component._get_all_app_wrap_components())
-
-            # Add the custom components from the page to the set.
-            custom_components |= component._get_all_custom_components()
-
-        progress.advance(task)
-
-        # Perform auto-memoization of stateful components.
-        (
-            stateful_components_path,
-            stateful_components_code,
-            page_components,
-        ) = compiler.compile_stateful_components(self.pages.values())
-
         progress.advance(task)
 
-        # Catch "static" apps (that do not define a rx.State subclass) which are trying to access rx.State.
-        if code_uses_state_contexts(stateful_components_code) and self.state is None:
-            raise ReflexRuntimeError(
-                "To access rx.State in frontend components, at least one "
-                "subclass of rx.State must be defined in the app."
-            )
-        compile_results.append((stateful_components_path, stateful_components_code))
-
         # Compile the root document before fork.
         compile_results.append(
             compiler.compile_document_root(
@@ -927,31 +908,12 @@ class App(MiddlewareMixin, LifespanMixin, Base):
             )
         )
 
-        # Compile the contexts before fork.
-        compile_results.append(
-            compiler.compile_contexts(self.state, self.theme),
-        )
         # Fix #2992 by removing the top-level appearance prop
         if self.theme is not None:
             self.theme.appearance = None
 
-        app_root = self._app_root(app_wrappers=app_wrappers)
-
         progress.advance(task)
 
-        # Prepopulate the global ExecutorSafeFunctions class with input data required by the compile functions.
-        # This is required for multiprocessing to work, in presence of non-picklable inputs.
-        for route, component in zip(self.pages, page_components):
-            ExecutorSafeFunctions.COMPILE_PAGE_ARGS_BY_ROUTE[route] = (
-                route,
-                component,
-                self.state,
-            )
-
-        ExecutorSafeFunctions.COMPILE_APP_APP_ROOT = app_root
-        ExecutorSafeFunctions.CUSTOM_COMPONENTS = custom_components
-        ExecutorSafeFunctions.STYLE = self.style
-
         # Use a forking process pool, if possible.  Much faster, especially for large sites.
         # Fallback to ThreadPoolExecutor as something that will always work.
         executor = None
@@ -969,36 +931,55 @@ class App(MiddlewareMixin, LifespanMixin, Base):
                 max_workers=environment.REFLEX_COMPILE_THREADS
             )
 
+        for route, component in self.pages.items():
+            component._add_style_recursive(self.style, self.theme)
+
+            ExecutorSafeFunctions.COMPONENTS[route] = component
+
+        for route, page in self.unevaluated_pages.items():
+            if route in self.pages:
+                continue
+
+            ExecutorSafeFunctions.UNCOMPILED_PAGES[route] = page
+
+        ExecutorSafeFunctions.STATE = self.state
+
+        pages_results = []
+
         with executor:
             result_futures = []
-            custom_components_future = None
-
-            def _mark_complete(_=None):
-                progress.advance(task)
+            pages_futures = []
 
             def _submit_work(fn, *args, **kwargs):
                 f = executor.submit(fn, *args, **kwargs)
-                f.add_done_callback(_mark_complete)
+                # f = executor.apipe(fn, *args, **kwargs)
                 result_futures.append(f)
 
             # Compile all page components.
-            for route in self.pages:
-                _submit_work(ExecutorSafeFunctions.compile_page, route)
-
-            # Compile the app wrapper.
-            _submit_work(ExecutorSafeFunctions.compile_app)
+            for route in self.unevaluated_pages:
+                if route in self.pages:
+                    continue
+
+                f = executor.submit(
+                    ExecutorSafeFunctions.compile_unevaluated_page,
+                    route,
+                    self.style,
+                    self.theme,
+                )
+                pages_futures.append(f)
 
-            # Compile the custom components.
-            custom_components_future = executor.submit(
-                ExecutorSafeFunctions.compile_custom_components,
-            )
-            custom_components_future.add_done_callback(_mark_complete)
+            # Compile the pre-compiled pages.
+            for route in self.pages:
+                _submit_work(
+                    ExecutorSafeFunctions.compile_page,
+                    route,
+                )
 
             # Compile the root stylesheet with base styles.
             _submit_work(compiler.compile_root_stylesheet, self.stylesheets)
 
             # Compile the theme.
-            _submit_work(ExecutorSafeFunctions.compile_theme)
+            _submit_work(compile_theme, self.style)
 
             # Compile the Tailwind config.
             if config.tailwind is not None:
@@ -1012,21 +993,70 @@ class App(MiddlewareMixin, LifespanMixin, Base):
             # Wait for all compilation tasks to complete.
             for future in concurrent.futures.as_completed(result_futures):
                 compile_results.append(future.result())
+                progress.advance(task)
+
+            for future in concurrent.futures.as_completed(pages_futures):
+                pages_results.append(future.result())
+                progress.advance(task)
+
+        for route, component, compiled_page in pages_results:
+            self._check_routes_conflict(route)
+            self.pages[route] = component
+            compile_results.append(compiled_page)
 
-            # Special case for custom_components, since we need the compiled imports
-            # to install proper frontend packages.
-            (
-                *custom_components_result,
-                custom_components_imports,
-            ) = custom_components_future.result()
-            compile_results.append(custom_components_result)
-            all_imports.update(custom_components_imports)
+        for _, component in self.pages.items():
+            # Add component._get_all_imports() to all_imports.
+            all_imports.update(component._get_all_imports())
+
+            # Add the app wrappers from this component.
+            app_wrappers.update(component._get_all_app_wrap_components())
+
+            # Add the custom components from the page to the set.
+            custom_components |= component._get_all_custom_components()
+
+        # Perform auto-memoization of stateful components.
+        (
+            stateful_components_path,
+            stateful_components_code,
+            page_components,
+        ) = compiler.compile_stateful_components(self.pages.values())
+
+        progress.advance(task)
+
+        # Catch "static" apps (that do not define a rx.State subclass) which are trying to access rx.State.
+        if code_uses_state_contexts(stateful_components_code) and self.state is None:
+            raise ReflexRuntimeError(
+                "To access rx.State in frontend components, at least one "
+                "subclass of rx.State must be defined in the app."
+            )
+        compile_results.append((stateful_components_path, stateful_components_code))
+
+        app_root = self._app_root(app_wrappers=app_wrappers)
 
         # Get imports from AppWrap components.
         all_imports.update(app_root._get_all_imports())
 
         progress.advance(task)
 
+        # Compile the contexts.
+        compile_results.append(
+            compiler.compile_contexts(self.state, self.theme),
+        )
+        progress.advance(task)
+
+        # Compile the app root.
+        compile_results.append(
+            compiler.compile_app(app_root),
+        )
+        progress.advance(task)
+
+        # Compile custom components.
+        *custom_components_result, custom_components_imports = (
+            compiler.compile_components(custom_components)
+        )
+        compile_results.append(custom_components_result)
+        all_imports.update(custom_components_imports)
+
         progress.advance(task)
         progress.stop()
 

+ 101 - 35
reflex/compiler/compiler.py

@@ -4,10 +4,11 @@ from __future__ import annotations
 
 from datetime import datetime
 from pathlib import Path
-from typing import Dict, Iterable, Optional, Type, Union
+from typing import TYPE_CHECKING, Dict, Iterable, Optional, Tuple, Type, Union
 
 from reflex import constants
 from reflex.compiler import templates, utils
+from reflex.components.base.fragment import Fragment
 from reflex.components.component import (
     BaseComponent,
     Component,
@@ -127,7 +128,7 @@ def _compile_contexts(state: Optional[Type[BaseState]], theme: Component | None)
 
 def _compile_page(
     component: Component,
-    state: Type[BaseState],
+    state: Type[BaseState] | None,
 ) -> str:
     """Compile the component given the app state.
 
@@ -142,7 +143,7 @@ def _compile_page(
     imports = utils.compile_imports(imports)
 
     # Compile the code to render the component.
-    kwargs = {"state_name": state.get_name()} if state else {}
+    kwargs = {"state_name": state.get_name()} if state is not None else {}
 
     return templates.PAGE.render(
         imports=imports,
@@ -424,7 +425,7 @@ def compile_contexts(
 
 
 def compile_page(
-    path: str, component: Component, state: Type[BaseState]
+    path: str, component: Component, state: Type[BaseState] | None
 ) -> tuple[str, str]:
     """Compile a single page.
 
@@ -534,6 +535,73 @@ def purge_web_pages_dir():
     utils.empty_dir(get_web_dir() / constants.Dirs.PAGES, keep_files=["_app.js"])
 
 
+if TYPE_CHECKING:
+    from reflex.app import UnevaluatedPage
+
+
+def compile_unevaluated_page(
+    route: str, page: UnevaluatedPage, state: Type[BaseState] | None = None
+) -> Tuple[Component, bool]:
+    """Compiles an uncompiled page into a component and adds meta information.
+
+    Args:
+        route: The route of the page.
+        page: The uncompiled page object.
+        state: The state of the app.
+
+    Returns:
+        The compiled component and whether state should be enabled.
+    """
+    # Generate the component if it is a callable.
+    component = page.component
+    component = component if isinstance(component, Component) else component()
+
+    # unpack components that return tuples in an rx.fragment.
+    if isinstance(component, tuple):
+        component = Fragment.create(*component)
+
+    enable_state = False
+    # Ensure state is enabled if this page uses state.
+    if state is None:
+        if page.on_load or component._has_stateful_event_triggers():
+            enable_state = True
+        else:
+            for var in component._get_vars(include_children=True):
+                var_data = var._get_all_var_data()
+                if not var_data:
+                    continue
+                if not var_data.state:
+                    continue
+                enable_state = True
+                break
+
+    from reflex.app import OverlayFragment
+    from reflex.utils.format import make_default_page_title
+
+    component = OverlayFragment.create(component)
+
+    meta_args = {
+        "title": (
+            page.title
+            if page.title is not None
+            else make_default_page_title(get_config().app_name, route)
+        ),
+        "image": page.image,
+        "meta": page.meta,
+    }
+
+    if page.description is not None:
+        meta_args["description"] = page.description
+
+    # Add meta information to the component.
+    utils.add_meta(
+        component,
+        **meta_args,
+    )
+
+    return component, enable_state
+
+
 class ExecutorSafeFunctions:
     """Helper class to allow parallelisation of parts of the compilation process.
 
@@ -559,13 +627,12 @@ class ExecutorSafeFunctions:
 
     """
 
-    COMPILE_PAGE_ARGS_BY_ROUTE = {}
-    COMPILE_APP_APP_ROOT: Component | None = None
-    CUSTOM_COMPONENTS: set[CustomComponent] | None = None
-    STYLE: ComponentStyle | None = None
+    COMPONENTS: Dict[str, Component] = {}
+    UNCOMPILED_PAGES: Dict[str, UnevaluatedPage] = {}
+    STATE: Optional[Type[BaseState]] = None
 
     @classmethod
-    def compile_page(cls, route: str):
+    def compile_page(cls, route: str) -> tuple[str, str]:
         """Compile a page.
 
         Args:
@@ -574,46 +641,45 @@ class ExecutorSafeFunctions:
         Returns:
             The path and code of the compiled page.
         """
-        return compile_page(*cls.COMPILE_PAGE_ARGS_BY_ROUTE[route])
+        return compile_page(route, cls.COMPONENTS[route], cls.STATE)
 
     @classmethod
-    def compile_app(cls):
-        """Compile the app.
-
-        Returns:
-            The path and code of the compiled app.
+    def compile_unevaluated_page(
+        cls,
+        route: str,
+        style: ComponentStyle,
+        theme: Component | None,
+    ) -> tuple[str, Component, tuple[str, str]]:
+        """Compile an unevaluated page.
 
-        Raises:
-            ValueError: If the app root is not set.
-        """
-        if cls.COMPILE_APP_APP_ROOT is None:
-            raise ValueError("COMPILE_APP_APP_ROOT should be set")
-        return compile_app(cls.COMPILE_APP_APP_ROOT)
-
-    @classmethod
-    def compile_custom_components(cls):
-        """Compile the custom components.
+        Args:
+            route: The route of the page to compile.
+            style: The style of the page.
+            theme: The theme of the page.
 
         Returns:
-            The path and code of the compiled custom components.
-
-        Raises:
-            ValueError: If the custom components are not set.
+            The route, compiled component, and compiled page.
         """
-        if cls.CUSTOM_COMPONENTS is None:
-            raise ValueError("CUSTOM_COMPONENTS should be set")
-        return compile_components(cls.CUSTOM_COMPONENTS)
+        component, enable_state = compile_unevaluated_page(
+            route, cls.UNCOMPILED_PAGES[route]
+        )
+        component = component if isinstance(component, Component) else component()
+        component._add_style_recursive(style, theme)
+        return route, component, compile_page(route, component, cls.STATE)
 
     @classmethod
-    def compile_theme(cls):
+    def compile_theme(cls, style: ComponentStyle | None) -> tuple[str, str]:
         """Compile the theme.
 
+        Args:
+            style: The style to compile.
+
         Returns:
             The path and code of the compiled theme.
 
         Raises:
             ValueError: If the style is not set.
         """
-        if cls.STYLE is None:
+        if style is None:
             raise ValueError("STYLE should be set")
-        return compile_theme(cls.STYLE)
+        return compile_theme(style)

+ 17 - 4
tests/units/test_app.py

@@ -237,9 +237,12 @@ def test_add_page_default_route(app: App, index_page, about_page):
         about_page: The about page.
     """
     assert app.pages == {}
+    assert app.unevaluated_pages == {}
     app.add_page(index_page)
+    app._compile_page("index")
     assert app.pages.keys() == {"index"}
     app.add_page(about_page)
+    app._compile_page("about")
     assert app.pages.keys() == {"index", "about"}
 
 
@@ -252,8 +255,9 @@ def test_add_page_set_route(app: App, index_page, windows_platform: bool):
         windows_platform: Whether the system is windows.
     """
     route = "test" if windows_platform else "/test"
-    assert app.pages == {}
+    assert app.unevaluated_pages == {}
     app.add_page(index_page, route=route)
+    app._compile_page("test")
     assert app.pages.keys() == {"test"}
 
 
@@ -267,8 +271,9 @@ def test_add_page_set_route_dynamic(index_page, windows_platform: bool):
     app = App(state=EmptyState)
     assert app.state is not None
     route = "/test/[dynamic]"
-    assert app.pages == {}
+    assert app.unevaluated_pages == {}
     app.add_page(index_page, route=route)
+    app._compile_page("test/[dynamic]")
     assert app.pages.keys() == {"test/[dynamic]"}
     assert "dynamic" in app.state.computed_vars
     assert app.state.computed_vars["dynamic"]._deps(objclass=EmptyState) == {
@@ -286,9 +291,9 @@ def test_add_page_set_route_nested(app: App, index_page, windows_platform: bool)
         windows_platform: Whether the system is windows.
     """
     route = "test\\nested" if windows_platform else "/test/nested"
-    assert app.pages == {}
+    assert app.unevaluated_pages == {}
     app.add_page(index_page, route=route)
-    assert app.pages.keys() == {route.strip(os.path.sep)}
+    assert app.unevaluated_pages.keys() == {route.strip(os.path.sep)}
 
 
 def test_add_page_invalid_api_route(app: App, index_page):
@@ -1238,6 +1243,7 @@ def test_overlay_component(
 
     app.add_page(rx.box("Index"), route="/test")
     # overlay components are wrapped during compile only
+    app._compile_page("test")
     app._setup_overlay_component()
     page = app.pages["test"]
 
@@ -1365,6 +1371,7 @@ def test_app_state_determination():
 
     # Add a page with `on_load` enables state.
     a1.add_page(rx.box("About"), route="/about", on_load=rx.console_log(""))
+    a1._compile_page("about")
     assert a1.state is not None
 
     a2 = App()
@@ -1372,6 +1379,7 @@ def test_app_state_determination():
 
     # Referencing a state Var enables state.
     a2.add_page(rx.box(rx.text(GenState.value)), route="/")
+    a2._compile_page("index")
     assert a2.state is not None
 
     a3 = App()
@@ -1379,6 +1387,7 @@ def test_app_state_determination():
 
     # Referencing router enables state.
     a3.add_page(rx.box(rx.text(State.router.page.full_path)), route="/")
+    a3._compile_page("index")
     assert a3.state is not None
 
     a4 = App()
@@ -1390,6 +1399,7 @@ def test_app_state_determination():
     a4.add_page(
         rx.box(rx.button("Click", on_click=DynamicState.on_counter)), route="/page2"
     )
+    a4._compile_page("page2")
     assert a4.state is not None
 
 
@@ -1469,6 +1479,9 @@ def test_add_page_component_returning_tuple():
     app.add_page(index)  # type: ignore
     app.add_page(page2)  # type: ignore
 
+    app._compile_page("index")
+    app._compile_page("page2")
+
     assert isinstance((fragment_wrapper := app.pages["index"].children[0]), Fragment)
     assert isinstance((first_text := fragment_wrapper.children[0]), Text)
     assert str(first_text.children[0].contents) == '"first"'  # type: ignore