diff --git a/reflex/.templates/apps/sidebar/code/sidebar.py b/reflex/.templates/apps/sidebar/code/sidebar.py index 985141212..1ef475d0f 100644 --- a/reflex/.templates/apps/sidebar/code/sidebar.py +++ b/reflex/.templates/apps/sidebar/code/sidebar.py @@ -7,5 +7,10 @@ from code.pages import * import reflex as rx -# Create the app and compile it. + +class State(rx.State): + """Define empty state to allow access to rx.State.router.""" + + +# Create the app. app = rx.App(style=styles.base_style) diff --git a/reflex/app.py b/reflex/app.py index 781c50df3..fabafd31e 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -63,6 +63,7 @@ from reflex.state import ( State, StateManager, StateUpdate, + code_uses_state_contexts, ) from reflex.utils import console, exceptions, format, prerequisites, types from reflex.utils.imports import ImportVar @@ -169,7 +170,8 @@ class App(Base): deprecation_version="0.3.5", removal_version="0.4.0", ) - self.state = State + if len(State.class_subclasses) > 0: + self.state = State # Get the config config = get_config() @@ -636,7 +638,11 @@ class App(Base): return def compile_(self): - """Compile the app and output it to the pages folder.""" + """Compile the app and output it to the pages folder. + + Raises: + RuntimeError: When any page uses state, but no rx.State subclass is defined. + """ # add the pages before the compile check so App know onload methods for render, kwargs in DECORATED_PAGES: self.add_page(render, **kwargs) @@ -701,6 +707,16 @@ class App(Base): stateful_components_code, page_components, ) = compiler.compile_stateful_components(self.pages.values()) + + # 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 RuntimeError( + "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)) result_futures = [] diff --git a/reflex/state.py b/reflex/state.py index 04e2554a5..4b66185bf 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -2181,3 +2181,15 @@ class ImmutableMutableProxy(MutableProxy): return super()._mark_dirty( wrapped=wrapped, instance=instance, args=args, kwargs=kwargs ) + + +def code_uses_state_contexts(javascript_code: str) -> bool: + """Check if the rendered Javascript uses state contexts. + + Args: + javascript_code: The Javascript code to check. + + Returns: + True if the code attempts to access a member of StateContexts. + """ + return bool("useContext(StateContexts" in javascript_code) diff --git a/reflex/testing.py b/reflex/testing.py index 83ff82b73..3656ab368 100644 --- a/reflex/testing.py +++ b/reflex/testing.py @@ -209,6 +209,8 @@ class AppHarness: reflex.config.get_config(reload=True) # reset rx.State subclasses State.class_subclasses.clear() + # Ensure the AppHarness test does not skip State assignment due to running via pytest + os.environ.pop(reflex.constants.PYTEST_CURRENT_TEST, None) # self.app_module.app. self.app_module = reflex.utils.prerequisites.get_compiled_app(reload=True) self.app_instance = self.app_module.app diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 71d5df8dc..0d196b3ba 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -159,6 +159,11 @@ def get_app(reload: bool = False) -> ModuleType: sys.path.insert(0, os.getcwd()) app = __import__(module, fromlist=(constants.CompileVars.APP,)) if reload: + from reflex.state import State + + # Reset rx.State subclasses to avoid conflict when reloading. + State.class_subclasses.clear() + # Reload the app module. importlib.reload(app) return app