Repository: reflex-dev/reflex
Pull request: apps should no longer call "app.compile()"
Closes #2293.

Two main tricks:
* To handle backend running, `gunicorn` or `uvicorn` to load a special shim `app_module_for_backend.py`, instead of the real app (e.g. `counter.py`).
* A new `get_compiled_app` function, which essentially does `get_app() + compile` - to account for the fact that `get_app()` will now return an app module with an **uncompiled** app.  We update existing call sites of `get_app()` with `get_compiled_app` accordingly, with some exceptions (when we really don't want to compile).

If we merge, release this, we need to clean up reflex-web and reflex-examples.

Problem statement:
Remove `app.compile()` boilerplate line in apps.
**Describe the bug**
Reflex apps require `app.compile()` line.  It would be better to not require that.

**To Reproduce**
Any existing reflex app in the wild.

**Expected behavior**
All existing reflex functionality works as usual, without `app.compile()` in apps.

Hints from issue discussion:

Commit message: get_compiled_app

Patch to review:
diff --git a/README.md b/README.md
index 0b301ea0d01..874c2de9216 100644
--- a/README.md
+++ b/README.md
@@ -123,7 +123,6 @@ def index():
 # Add state and page to the app.
 app = rx.App()
 app.add_page(index, title="reflex:DALL·E")
-app.compile()
 ```
 
 ## Let's break this down.
diff --git a/docs/es/README.md b/docs/es/README.md
index dd82b746108..74c3e24a76c 100644
--- a/docs/es/README.md
+++ b/docs/es/README.md
@@ -121,7 +121,6 @@ def index():
 # Add state and page to the app.
 app = rx.App()
 app.add_page(index, title="reflex:DALL·E")
-app.compile()
 ```
 
 ## Vamos a desglosarlo.
@@ -191,7 +190,6 @@ Añadimos una página desde la raíz (root) de la aplicación al componente de 
 
 ```python
 app.add_page(index, title="DALL-E")
-app.compile()
 ```
 
 Puedes crear una aplicación multipágina añadiendo más páginas.
diff --git a/docs/in/README.md b/docs/in/README.md
index 220504ef4d2..12e662f1167 100644
--- a/docs/in/README.md
+++ b/docs/in/README.md
@@ -120,7 +120,6 @@ def index():
 # Add state and page to the app.
 app = rx.App()
 app.add_page(index, title="reflex:DALL·E")
-app.compile()
 ```
 
 ## चलो इसे विस्तार से देखते हैं।
@@ -190,7 +189,6 @@ app = rx.App()
 
 ```python
 app.add_page(index, title="DALL-E")
-app.compile()
 ```
 
 आप और पेज जोड़कर एक मल्टी-पेज एप्लिकेशन बना सकते हैं।
diff --git a/docs/it/README.md b/docs/it/README.md
index 04e3f1e9b8f..77274ae0337 100644
--- a/docs/it/README.md
+++ b/docs/it/README.md
@@ -121,7 +121,6 @@ def index():
 # Aggiungi stato e pagina all'app.
 app = rx.App()
 app.add_page(index, title="reflex:DALL·E")
-app.compile()
 ```
 
 ## Analizziamolo
@@ -191,7 +190,6 @@ Possiamo aggiungere una pagina dalla radice dell'app al componente dell'indice.A
 
 ```python
 app.add_page(index, title="DALL-E")
-app.compile()
 ```
 
 Puoi creare un'app multi-pagina aggiungendo altre pagine.
diff --git a/docs/kr/README.md b/docs/kr/README.md
index b671458d631..2e443c855f8 100644
--- a/docs/kr/README.md
+++ b/docs/kr/README.md
@@ -121,7 +121,6 @@ def index():
 # Add state and page to the app.
 app = rx.App()
 app.add_page(index, title="reflex:DALL·E")
-app.compile()
 ```
 
 ## 하나씩 살펴보겠습니다.
@@ -194,7 +193,6 @@ app = rx.App()
 
 ```python
 app.add_page(index, title="DALL-E")
-app.compile()
 ```
 
 여러 페이지를 추가하여 멀티 페이지 앱을 만들 수 있습니다.
diff --git a/docs/pt/pt_br/README.md b/docs/pt/pt_br/README.md
index ac69f009fae..96708a75b29 100644
--- a/docs/pt/pt_br/README.md
+++ b/docs/pt/pt_br/README.md
@@ -121,7 +121,6 @@ def index():
 # Adição do estado e da página no app.
 app = rx.App()
 app.add_page(index, title="reflex:DALL·E")
-app.compile()
 ```
 
 ## Vamos por partes.
@@ -192,7 +191,6 @@ Adicionamos uma página na raíz do app, apontando para o componente index. Tamb
 
 ```python
 app.add_page(index, title="DALL-E")
-app.compile()
 ```
 
 Você pode criar mais páginas e adicioná-las ao seu app.
diff --git a/docs/tr/README.md b/docs/tr/README.md
index 7215b4226fe..657fb87059a 100644
--- a/docs/tr/README.md
+++ b/docs/tr/README.md
@@ -121,7 +121,6 @@ def index():
 # Add state and page to the app.
 app = rx.App()
 app.add_page(index, title="reflex:DALL·E")
-app.compile()
 ```
 
 ## Hadi bunu parçalara ayırarak inceleyelim.
@@ -192,7 +191,6 @@ Root'tan index bileşenlerine bir sayfa ekliyoruz. Ayrıca sayfa önizlemesi/tar
 
 ```python
 app.add_page(index, title="DALL-E")
-app.compile()
 ```
 
 Daha fazla sayfa ekleyerek çok sayfalı bir uygulama oluşturabilirsiniz.
diff --git a/docs/zh/zh_cn/README.md b/docs/zh/zh_cn/README.md
index bf6dbe179b4..6e6da1282c4 100644
--- a/docs/zh/zh_cn/README.md
+++ b/docs/zh/zh_cn/README.md
@@ -122,7 +122,6 @@ def index():
 # Add state and page to the app.
 app = rx.App()
 app.add_page(index, title="reflex:DALL·E")
-app.compile()
 ```
 
 ## 让我们分解以上步骤.
@@ -194,7 +193,6 @@ app = rx.App()
 
 ```python
 app.add_page(index, title="DALL-E")
-app.compile()
 ```
 
 您可以通过增加更多页面来创建一个多页面的应用.
diff --git a/docs/zh/zh_tw/README.md b/docs/zh/zh_tw/README.md
index 098f661a620..ced699f4715 100644
--- a/docs/zh/zh_tw/README.md
+++ b/docs/zh/zh_tw/README.md
@@ -121,7 +121,6 @@ def index():
 # 把狀態跟頁面添加到應用程式。
 app = rx.App()
 app.add_page(index, title="reflex:DALL·E")
-app.compile()
 ```
 
 ## 讓我們來拆解一下。
@@ -192,7 +191,6 @@ app = rx.App()
 
 ```python
 app.add_page(index, title="DALL-E")
-app.compile()
 ```
 
 你可以添加更多頁面至路由藉此來建立多頁面應用程式(multi-page app)
diff --git a/integration/test_background_task.py b/integration/test_background_task.py
index bc70ff01fba..799d68c2f67 100644
--- a/integration/test_background_task.py
+++ b/integration/test_background_task.py
@@ -95,7 +95,6 @@ def index() -> rx.Component:
 
     app = rx.App(state=rx.State)
     app.add_page(index)
-    app.compile()
 
 
 @pytest.fixture(scope="session")
diff --git a/integration/test_call_script.py b/integration/test_call_script.py
index 95bdf37c998..c3a36430668 100644
--- a/integration/test_call_script.py
+++ b/integration/test_call_script.py
@@ -227,8 +227,6 @@ def index():
             rx.button("Reset", id="reset", on_click=CallScriptState.reset_),
         )
 
-    app.compile()
-
 
 @pytest.fixture(scope="session")
 def call_script(tmp_path_factory) -> Generator[AppHarness, None, None]:
diff --git a/integration/test_client_storage.py b/integration/test_client_storage.py
index a9a311d8c16..2678f3d52cf 100644
--- a/integration/test_client_storage.py
+++ b/integration/test_client_storage.py
@@ -100,7 +100,6 @@ def index():
     app = rx.App(state=rx.State)
     app.add_page(index)
     app.add_page(index, route="/foo")
-    app.compile()
 
 
 @pytest.fixture(scope="session")
diff --git a/integration/test_connection_banner.py b/integration/test_connection_banner.py
index 0468e6b535f..293d2e4126d 100644
--- a/integration/test_connection_banner.py
+++ b/integration/test_connection_banner.py
@@ -21,7 +21,6 @@ def index():
 
     app = rx.App(state=rx.State)
     app.add_page(index)
-    app.compile()
 
 
 @pytest.fixture()
diff --git a/integration/test_dynamic_routes.py b/integration/test_dynamic_routes.py
index cafb9c61ba6..7cb64754f88 100644
--- a/integration/test_dynamic_routes.py
+++ b/integration/test_dynamic_routes.py
@@ -66,7 +66,6 @@ def redirect_page():
     app.add_page(index, route="/page/[page_id]", on_load=DynamicState.on_load)  # type: ignore
     app.add_page(index, route="/static/x", on_load=DynamicState.on_load)  # type: ignore
     app.add_custom_404_page(on_load=DynamicState.on_load)  # type: ignore
-    app.compile()
 
 
 @pytest.fixture(scope="session")
diff --git a/integration/test_event_actions.py b/integration/test_event_actions.py
index da444a5cfe7..89c8ece22d6 100644
--- a/integration/test_event_actions.py
+++ b/integration/test_event_actions.py
@@ -132,7 +132,6 @@ def index():
 
     app = rx.App(state=rx.State)
     app.add_page(index)
-    app.compile()
 
 
 @pytest.fixture(scope="session")
diff --git a/integration/test_event_chain.py b/integration/test_event_chain.py
index 7d003635e2c..ec3f125b428 100644
--- a/integration/test_event_chain.py
+++ b/integration/test_event_chain.py
@@ -242,8 +242,6 @@ def on_mount_yield_chain():
     app.add_page(on_mount_return_chain)
     app.add_page(on_mount_yield_chain)
 
-    app.compile()
-
 
 @pytest.fixture(scope="session")
 def event_chain(tmp_path_factory) -> Generator[AppHarness, None, None]:
diff --git a/integration/test_form_submit.py b/integration/test_form_submit.py
index 8d36cc01dd5..b64846c1b12 100644
--- a/integration/test_form_submit.py
+++ b/integration/test_form_submit.py
@@ -62,8 +62,6 @@ def index():
             height="100vh",
         )
 
-    app.compile()
-
 
 def FormSubmitName():
     """App with a form using on_submit."""
@@ -124,8 +122,6 @@ def index():
             height="100vh",
         )
 
-    app.compile()
-
 
 @pytest.fixture(
     scope="session", params=[FormSubmit, FormSubmitName], ids=["id", "name"]
diff --git a/integration/test_input.py b/integration/test_input.py
index 2ca15424e44..111124349db 100644
--- a/integration/test_input.py
+++ b/integration/test_input.py
@@ -40,8 +40,6 @@ def index():
             rx.button("CLEAR", on_click=rx.set_value("on_change_input", "")),
         )
 
-    app.compile()
-
 
 @pytest.fixture()
 def fully_controlled_input(tmp_path) -> Generator[AppHarness, None, None]:
diff --git a/integration/test_login_flow.py b/integration/test_login_flow.py
index f5363574378..a4272fe39c3 100644
--- a/integration/test_login_flow.py
+++ b/integration/test_login_flow.py
@@ -45,7 +45,6 @@ def login():
     app = rx.App(state=rx.State)
     app.add_page(index)
     app.add_page(login)
-    app.compile()
 
 
 @pytest.fixture(scope="session")
diff --git a/integration/test_server_side_event.py b/integration/test_server_side_event.py
index 31a38ba36c8..b8e8d21c00c 100644
--- a/integration/test_server_side_event.py
+++ b/integration/test_server_side_event.py
@@ -75,8 +75,6 @@ def index():
             ),
         )
 
-    app.compile()
-
 
 @pytest.fixture(scope="session")
 def server_side_event(tmp_path_factory) -> Generator[AppHarness, None, None]:
diff --git a/integration/test_table.py b/integration/test_table.py
index 00e6a8b22d1..560d70b9413 100644
--- a/integration/test_table.py
+++ b/integration/test_table.py
@@ -88,8 +88,6 @@ def another():
             )
         )
 
-    app.compile()
-
 
 @pytest.fixture()
 def table(tmp_path_factory) -> Generator[AppHarness, None, None]:
diff --git a/integration/test_upload.py b/integration/test_upload.py
index 13fb28d36a2..c832f5acf95 100644
--- a/integration/test_upload.py
+++ b/integration/test_upload.py
@@ -115,7 +115,6 @@ def index():
 
     app = rx.App(state=rx.State)
     app.add_page(index)
-    app.compile()
 
 
 @pytest.fixture(scope="session")
diff --git a/integration/test_var_operations.py b/integration/test_var_operations.py
index a0f245fe4f1..4c56c01f507 100644
--- a/integration/test_var_operations.py
+++ b/integration/test_var_operations.py
@@ -565,8 +565,6 @@ def index():
             ),
         )
 
-    app.compile()
-
 
 @pytest.fixture(scope="session")
 def var_operations(tmp_path_factory) -> Generator[AppHarness, None, None]:
diff --git a/reflex/app.py b/reflex/app.py
index 63930acea15..6d9582f41a5 100644
--- a/reflex/app.py
+++ b/reflex/app.py
@@ -619,6 +619,24 @@ def _should_compile(self) -> bool:
         return True
 
     def compile(self):
+        """App modules no longer need to call this.
+
+        Reflex framework will perform compile_() as needed.
+
+        Existing call sites will get a deprecation message.
+
+        We will eventually remove app.compile() method entirely.
+        """
+        console.deprecate(
+            feature_name="app.compile()",
+            reason="Explicit calls to app.compile() are not needed."
+            " Method will be removed in 0.3.9",
+            deprecation_version="0.3.7",
+            removal_version="0.3.9",
+        )
+        return
+
+    def compile_(self):
         """Compile the app and output it to the pages folder."""
         # add the pages before the compile check so App know onload methods
         for render, kwargs in DECORATED_PAGES:
diff --git a/reflex/app.pyi b/reflex/app.pyi
index b3b77bd2049..38175ca2887 100644
--- a/reflex/app.pyi
+++ b/reflex/app.pyi
@@ -120,6 +120,7 @@ class App(Base):
     def setup_admin_dash(self) -> None: ...
     def get_frontend_packages(self, imports: Dict[str, str]): ...
     def compile(self) -> None: ...
+    def compile_(self) -> None: ...
     def modify_state(self, token: str) -> AsyncContextManager[State]: ...
     def _process_background(
         self, state: State, event: Event
diff --git a/reflex/app_module_for_backend.py b/reflex/app_module_for_backend.py
new file mode 100644
index 00000000000..4a5b107194c
--- /dev/null
+++ b/reflex/app_module_for_backend.py
@@ -0,0 +1,13 @@
+"""Shims the real reflex app module for running backend server (uvicorn or gunicorn).
+Only the app attribute is explicitly exposed.
+"""
+from reflex import constants
+from reflex.utils.prerequisites import get_compiled_app
+
+if "app" != constants.CompileVars.APP:
+    raise AssertionError("unexpected variable name for 'app'")
+app = getattr(get_compiled_app(), constants.CompileVars.APP)
+
+# ensure only "app" is exposed.
+del get_compiled_app
+del constants
diff --git a/reflex/reflex.py b/reflex/reflex.py
index 5c185eb0400..8aae6a8bb18 100644
--- a/reflex/reflex.py
+++ b/reflex/reflex.py
@@ -178,7 +178,7 @@ def _run(
     if frontend:
         prerequisites.update_next_config()
         # Get the app module.
-        prerequisites.get_app()
+        prerequisites.get_compiled_app()
 
     # Warn if schema is not up to date.
     prerequisites.check_schema_up_to_date()
@@ -362,7 +362,7 @@ def db_init():
 
     # Initialize the database.
     _skip_compile()
-    prerequisites.get_app()
+    prerequisites.get_compiled_app()
     model.Model.alembic_init()
     model.Model.migrate(autogenerate=True)
 
@@ -373,8 +373,9 @@ def migrate():
     from reflex import model
     from reflex.utils import prerequisites
 
+    # TODO see if we can use `get_app()` instead (no compile).  Would _skip_compile still be needed then?
     _skip_compile()
-    prerequisites.get_app()
+    prerequisites.get_compiled_app()
     if not prerequisites.check_db_initialized():
         return
     model.Model.migrate()
@@ -393,8 +394,9 @@ def makemigrations(
     from reflex import model
     from reflex.utils import prerequisites
 
+    # TODO see if we can use `get_app()` instead (no compile).  Would _skip_compile still be needed then?
     _skip_compile()
-    prerequisites.get_app()
+    prerequisites.get_compiled_app()
     if not prerequisites.check_db_initialized():
         return
     with model.Model.get_db_engine().connect() as connection:
diff --git a/reflex/state.py b/reflex/state.py
index db756d5c217..04e2554a5ad 100644
--- a/reflex/state.py
+++ b/reflex/state.py
@@ -1316,6 +1316,7 @@ def on_load_internal(self) -> list[Event | EventSpec] | None:
         Returns:
             The list of events to queue for on load handling.
         """
+        # Do not app.compile_()!  It should be already compiled by now.
         app = getattr(prerequisites.get_app(), constants.CompileVars.APP)
         load_events = app.get_load_events(self.router.page.path)
         if not load_events and self.is_hydrated:
@@ -1364,6 +1365,7 @@ def __init__(self, state_instance):
             state_instance: The state instance to proxy.
         """
         super().__init__(state_instance)
+        # compile is not relevant to backend logic
         self._self_app = getattr(prerequisites.get_app(), constants.CompileVars.APP)
         self._self_substate_path = state_instance.get_full_name().split(".")
         self._self_actx = None
diff --git a/reflex/testing.py b/reflex/testing.py
index 4e32f52b390..63a56ff06df 100644
--- a/reflex/testing.py
+++ b/reflex/testing.py
@@ -165,7 +165,7 @@ def _initialize_app(self):
             # reset rx.State subclasses
             State.class_subclasses.clear()
             # self.app_module.app.
-            self.app_module = reflex.utils.prerequisites.get_app(reload=True)
+            self.app_module = reflex.utils.prerequisites.get_compiled_app(reload=True)
         self.app_instance = self.app_module.app
         if isinstance(self.app_instance.state_manager, StateManagerRedis):
             # Create our own redis connection for testing.
diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py
index 04b225a18d2..a992534b450 100644
--- a/reflex/utils/exec.py
+++ b/reflex/utils/exec.py
@@ -160,7 +160,7 @@ def run_backend(
     import uvicorn
 
     config = get_config()
-    app_module = f"{config.app_name}.{config.app_name}:{constants.CompileVars.APP}"
+    app_module = f"reflex.app_module_for_backend:{constants.CompileVars.APP}"
 
     # Create a .nocompile file to skip compile for backend.
     if os.path.exists(constants.Dirs.WEB):
@@ -196,7 +196,7 @@ def run_backend_prod(
     config = get_config()
     RUN_BACKEND_PROD = f"gunicorn --worker-class {config.gunicorn_worker_class} --preload --timeout {config.timeout} --log-level critical".split()
     RUN_BACKEND_PROD_WINDOWS = f"uvicorn --timeout-keep-alive {config.timeout}".split()
-    app_module = f"{config.app_name}.{config.app_name}:{constants.CompileVars.APP}"
+    app_module = f"reflex.app_module_for_backend:{constants.CompileVars.APP}"
     command = (
         [
             *RUN_BACKEND_PROD_WINDOWS,
diff --git a/reflex/utils/export.py b/reflex/utils/export.py
index f2b8e69a80a..12a6933fbd8 100644
--- a/reflex/utils/export.py
+++ b/reflex/utils/export.py
@@ -56,7 +56,7 @@ def export(
         # Update some parameters for export
         prerequisites.update_next_config(export=True)
         # Ensure module can be imported and app.compile() is called.
-        prerequisites.get_app()
+        prerequisites.get_compiled_app()
         # Set up .web directory and install frontend dependencies.
         build.setup_frontend(Path.cwd())
 
diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py
index 9d542e9a683..e4943837a2c 100644
--- a/reflex/utils/prerequisites.py
+++ b/reflex/utils/prerequisites.py
@@ -159,11 +159,25 @@ def get_app(reload: bool = False) -> ModuleType:
     sys.path.insert(0, os.getcwd())
     app = __import__(module, fromlist=(constants.CompileVars.APP,))
     if reload:
-
         importlib.reload(app)
+
     return app
 
 
+def get_compiled_app(reload: bool = False) -> ModuleType:
+    """Get the app module based on the default config after first compiling it.
+
+    Args:
+        reload: Re-import the app module from disk
+
+    Returns:
+        The compiled app based on the default config.
+    """
+    app_module = get_app(reload=reload)
+    getattr(app_module, constants.CompileVars.APP).compile_()
+    return app_module
+
+
 def get_redis() -> Redis | None:
     """Get the redis client.
 
diff --git a/tests/test_app.py b/tests/test_app.py
index eb1b783ed88..ac402853622 100644
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -1213,7 +1213,7 @@ def test_app_wrap_compile_theme(compilable_app):
     """
     app, web_dir = compilable_app
     app.theme = rdxt.theme(accent_color="plum")
-    app.compile()
+    app.compile_()
     app_js_contents = (web_dir / "pages" / "_app.js").read_text()
     app_js_lines = [
         line.strip() for line in app_js_contents.splitlines() if line.strip()
@@ -1263,7 +1263,7 @@ def page():
         return Fragment1.create(Fragment3.create())
 
     app.add_page(page)
-    app.compile()
+    app.compile_()
     app_js_contents = (web_dir / "pages" / "_app.js").read_text()
     app_js_lines = [
         line.strip() for line in app_js_contents.splitlines() if line.strip()
diff --git a/tests/test_state.py b/tests/test_state.py
index de9187efce6..423886655dd 100644
--- a/tests/test_state.py
+++ b/tests/test_state.py
@@ -1590,7 +1590,11 @@ def mock_app(monkeypatch, state_manager: StateManager) -> rx.App:
     app.state = TestState
     app._state_manager = state_manager
     app.event_namespace.emit = AsyncMock()  # type: ignore
-    monkeypatch.setattr(prerequisites, "get_app", lambda: app_module)
+
+    def _mock_get_app(*args, **kwargs):
+        return app_module
+
+    monkeypatch.setattr(prerequisites, "get_app", _mock_get_app)
     return app
 
 
diff --git a/tests/test_testing.py b/tests/test_testing.py
index ff87534ba52..10b3d96632d 100644
--- a/tests/test_testing.py
+++ b/tests/test_testing.py
@@ -21,7 +21,7 @@ class State(rx.State):
 
         app = rx.App(state=State)
         app.add_page(lambda: rx.text("Basic App"), route="/", title="index")
-        app.compile()
+        app.compile_()
 
     with AppHarness.create(
         root=tmp_path,


Code review comments:
diff_hunk: @@ -619,6 +619,24 @@ def _should_compile(self) -> bool:
         return True
 
     def compile(self):
+        """App modules no longer need to call this.
+
+        Reflex framework will perform compile_() as needed.
+
+        Existing call sites will get a deprecation message.
+
+        We will eventually remove app.compile() method entirely.
+        """
+        console.deprecate(
+            feature_name="app.compile()",
+            reason="Explicit calls to app.compile() are not needed."
+            " Method will be removed in 0.3.9",
+            deprecation_version="0.3.7",
+            removal_version="0.3.9",
path: reflex/app.py
text: @user1:
Mark the removal to 0.4.0 to sync with other planned removals.