diff --git a/docs/advanced_onboarding/code_structure.md b/docs/advanced_onboarding/code_structure.md index 025f79837d7..f8749953bee 100644 --- a/docs/advanced_onboarding/code_structure.md +++ b/docs/advanced_onboarding/code_structure.md @@ -13,6 +13,10 @@ The main app module is responsible for importing all other modules that make up As applications scale, effective organization is crucial. This is achieved by breaking the application down into smaller, manageable modules and organizing them into logical packages that avoid circular dependencies. +The examples below use page and component packages to introduce the mechanics. For a larger application, prefer +feature packages that keep each page or workflow close to its State, events, components, services, and tests. See +[Scaling State](/docs/state-structure/scaling-state) for the recommended feature layout and State ownership rules. + In the following documentation there will be an app with an `app_name` of `example_big_app`. The main module would be `example_big_app/example_big_app.py`. In the [Putting it all together](#putting-it-all-together) section there is a visual of the project folder structure to help follow along with the examples below. @@ -31,10 +35,11 @@ import reflex as rx from ..state import AuthState -class LoginState(AuthState): +class LoginState(rx.State): @rx.event - def handle_submit(self, form_data): - self.logged_in = authenticate(form_data["username"], form_data["password"]) + async def handle_submit(self, form_data): + auth = await self.get_state(AuthState) + auth.logged_in = authenticate(form_data["username"], form_data["password"]) def login_field(name: str, **input_props): @@ -99,6 +104,9 @@ Most pages will use State in some capacity. You should avoid adding vars to a shared state that will only be used in a single page. Instead, define a new subclass of `rx.State` and keep it in the same module as the page. +As the page grows, its page function and State may move into separate modules inside the same feature package. Keep +the State directly under `rx.State` unless a parent-child loading relationship is intentional. + ### Accessing other States As of Reflex 0.4.3, any event handler can get access to an instance of any other @@ -210,8 +218,9 @@ module should not import other modules in the app. The primary mechanism for reusing components in Reflex is to define a function that returns the component, then simply call it where that functionality is needed. -Component functions typically should not take any State classes as arguments, but prefer -to import the needed state and access the vars on the class directly. +A component used only inside one feature may import and bind directly to that feature's State. +A component shared between features should accept the values and event handlers it needs rather +than importing or accepting an application State class. ### Memoize Functions for Improved Performance diff --git a/docs/ai_builder/integrations/agent_toolkit.md b/docs/ai_builder/integrations/agent_toolkit.md index e6268b4984e..cf8eab40315 100644 --- a/docs/ai_builder/integrations/agent_toolkit.md +++ b/docs/ai_builder/integrations/agent_toolkit.md @@ -63,6 +63,7 @@ You do not need an API key to read Reflex documentation. Start by deciding how y - For local app development, use Python 3.10 or newer and a project virtual environment. - For current documentation context, give the assistant Markdown docs or `llms.txt`. +- For a large or multi-page app, start with [Scaling State](/docs/state-structure/scaling-state/), [Project Structure (Advanced)](/docs/advanced-onboarding/code-structure/), and [State Structure](/docs/state-structure/overview/). - For structured tool access, use the Reflex MCP integration. - For repeatable agent behavior, install Reflex Agent Skills. - For a browser-based AI builder, use Reflex Build. @@ -83,6 +84,8 @@ https://reflex.dev/docs/ai/integrations/agent-toolkit.md Use this when an agent needs one focused page. +For architecture work in a large or multi-page app, send the agent directly to the [Scaling State Markdown page](https://reflex.dev/docs/state-structure/scaling-state.md). It links the State ownership decisions to the advanced project-structure and State-structure guidance. + ## llms.txt @@ -149,6 +152,13 @@ Work on this existing Reflex app. First inspect the project structure and curren ``` +## Large App + +```text +Plan or refactor this large, multi-page Reflex app. Before changing code, read the current Scaling State, Project Structure (Advanced), and State Structure guides. Map page and feature ownership plus cross-State dependencies, choose boundaries using those guides, make the smallest coherent change, and validate it with reflex compile --dry and the project's tests. +``` + + ## Debugging ```text diff --git a/docs/ai_builder/integrations/agents_md.md b/docs/ai_builder/integrations/agents_md.md index e5c271d775d..3af83619443 100644 --- a/docs/ai_builder/integrations/agents_md.md +++ b/docs/ai_builder/integrations/agents_md.md @@ -65,10 +65,13 @@ The template covers Reflex-wide setup. Below it, add anything else the assistant - Internal conventions and code style. - Required lint, type-check, or test commands. - Folder layout and where new code should go. +- State ownership, page or feature boundaries, and permitted cross-State dependencies. - Hosting or deployment notes. Keep entries short and imperative — assistants follow concise, direct instructions more reliably than long paragraphs. +For large or multi-page apps, direct the assistant to read [Scaling State](/docs/state-structure/scaling-state/), [Project Structure (Advanced)](/docs/advanced-onboarding/code-structure/), and [State Structure](/docs/state-structure/overview/) before it introduces State inheritance or reorganizes modules. + ## Keeping Files Updated Reflex evolves quickly. If `reflex init` created your `AGENTS.md`, re-running `reflex init` refreshes the content between the managed markers while preserving everything you added outside them. diff --git a/docs/ai_builder/integrations/skills.md b/docs/ai_builder/integrations/skills.md index 8c1e4ab707f..e6c06a8e43a 100644 --- a/docs/ai_builder/integrations/skills.md +++ b/docs/ai_builder/integrations/skills.md @@ -18,7 +18,7 @@ def skills_summary_cards() -> rx.Component: _summary_card( "Docs", "Current Reflex guidance", - "Point agents to the right Reflex docs for state, vars, components, routing, styling, deployment, and more.", + "Point agents to the right Reflex docs for state architecture, large-app structure, vars, components, routing, styling, deployment, and more.", ), _summary_card( "Setup", @@ -52,6 +52,7 @@ skills_summary_cards() Use Reflex Agent Skills when you want an AI assistant to follow Reflex-specific workflows instead of relying only on general training data. They are especially useful when an assistant needs to: - Build or edit a Reflex app. +- Plan or refactor State boundaries in a large, multi-page app. - Set up a new Python environment. - Decide which Reflex docs apply to the task. - Compile, run, restart, or debug a local Reflex server. @@ -166,6 +167,16 @@ The `reflex-docs` skill gives the assistant a Reflex-specific reference map and It also reminds the assistant to prefer current Reflex documentation over pre-trained knowledge when there is a conflict. +### Large Apps + +When using the docs skill for a large or multi-page app, give the assistant these pages before it creates new State inheritance or reorganizes modules: + +1. [Scaling State](/docs/state-structure/scaling-state/) +2. [Project Structure (Advanced)](/docs/advanced-onboarding/code-structure/) +3. [State Structure](/docs/state-structure/overview/) + +This sequence gives the assistant the architecture decision guide first, followed by the module-layout and State API details needed to apply it. + ## Setup @@ -253,7 +264,8 @@ Using `-sTCP:LISTEN` helps the assistant target the server process instead of br 1. Open your Reflex project in an agent-enabled editor. 2. Ask for the feature, bug fix, or refactor you want. 3. The assistant should load `reflex-docs` when it sees Reflex code. -4. For local verification, the assistant should compile the app with `reflex compile --dry` or run it with the process-management workflow. +4. For a multi-page app or substantial State refactor, the assistant should read [Scaling State](/docs/state-structure/scaling-state/) and its linked structure guides before changing architecture. +5. For local verification, the assistant should compile the app with `reflex compile --dry` or run it with the process-management workflow. ## Debugging diff --git a/docs/app/agent_files/_plugin.py b/docs/app/agent_files/_plugin.py index 4a11adb4c72..187f0a584e2 100644 --- a/docs/app/agent_files/_plugin.py +++ b/docs/app/agent_files/_plugin.py @@ -35,6 +35,14 @@ > Reflex is a Python framework for building full-stack web apps. Use this index to find agent-readable Markdown docs, or see [llms-full.txt]({llms_full_txt_url}) for the complete docs in one file. +## Large App Architecture + +For multi-page apps or apps with substantial state, read these guides before choosing module and State boundaries: + +1. [Scaling State]({scaling_state_url}) +2. [Project Structure (Advanced)]({code_structure_url}) +3. [State Structure]({state_structure_url}) + ## Docs """ @@ -45,16 +53,24 @@ This file stitches together the full Reflex documentation as Markdown for AI agents and LLM indexing. For a navigable index with links to individual docs pages, see [llms.txt]({llms_txt_url}). + +For multi-page apps or apps with substantial state, start with [Scaling State]({scaling_state_url}), then review [Project Structure (Advanced)]({code_structure_url}) and [State Structure]({state_structure_url}) before choosing module and State boundaries. """ MARKDOWN_DIRECTIVE = ( "> For AI agents: the complete documentation index is at " "[llms.txt]({llms_txt_url}). Markdown versions are available by appending " - "`.md` or sending `Accept: text/markdown`." + "`.md` or sending `Accept: text/markdown`. For large or multi-page apps, " + "start with [Scaling State]({scaling_state_url})." ) PUBLIC_LLMS_TXT_URL = "https://reflex.dev/docs/llms.txt" +PUBLIC_SCALING_STATE_URL = "https://reflex.dev/docs/state-structure/scaling-state.md" PUBLIC_EVENT_TRIGGERS_URL = "https://reflex.dev/docs/api-reference/event-triggers/" +SCALING_STATE_DOC_PATH = Path("state-structure/scaling-state.md") +CODE_STRUCTURE_DOC_PATH = Path("advanced-onboarding/code-structure.md") +STATE_STRUCTURE_DOC_PATH = Path("state-structure/overview.md") + @dataclass(frozen=True) class MarkdownFileEntry: @@ -271,7 +287,10 @@ def _markdown_directive() -> str: Returns: The markdown blockquote directive. """ - return MARKDOWN_DIRECTIVE.format(llms_txt_url=PUBLIC_LLMS_TXT_URL).strip() + return MARKDOWN_DIRECTIVE.format( + llms_txt_url=PUBLIC_LLMS_TXT_URL, + scaling_state_url=PUBLIC_SCALING_STATE_URL, + ).strip() def generate_markdown_file_content(entry: MarkdownFileEntry) -> str: @@ -741,6 +760,9 @@ def generate_llms_txt( lines = [ LLMS_TXT_INTRO.format( llms_full_txt_url=_llms_url_for_path(Path("llms-full.txt")), + scaling_state_url=_llms_url_for_path(SCALING_STATE_DOC_PATH), + code_structure_url=_llms_url_for_path(CODE_STRUCTURE_DOC_PATH), + state_structure_url=_llms_url_for_path(STATE_STRUCTURE_DOC_PATH), ).strip(), "", ] @@ -773,6 +795,9 @@ def generate_llms_full_txt( LLMS_FULL_INTRO.format( docs_home_url=_docs_home_url(), llms_txt_url=_llms_url_for_path(Path("llms.txt")), + scaling_state_url=_llms_url_for_path(SCALING_STATE_DOC_PATH), + code_structure_url=_llms_url_for_path(CODE_STRUCTURE_DOC_PATH), + state_structure_url=_llms_url_for_path(STATE_STRUCTURE_DOC_PATH), ).strip(), "", ] diff --git a/docs/app/reflex_docs/redirects.py b/docs/app/reflex_docs/redirects.py new file mode 100644 index 00000000000..3aabdbb0ce4 --- /dev/null +++ b/docs/app/reflex_docs/redirects.py @@ -0,0 +1,34 @@ +"""Redirect mappings for the docs site.""" + +from collections.abc import Iterable + +from reflex_site_shared.route import Route + + +def get_redirects(routes: Iterable[Route]) -> list[tuple[str, str]]: + """Return static redirects and aliases generated from registered routes. + + Args: + routes: Route subset used to generate ``/ai-builder/`` aliases. + + Returns: + All static redirects plus aliases for the supplied route subset. + """ + return [ + ("/ai/integrations/ai-onboarding/", "/ai/integrations/agent-toolkit/"), + ("/ai-builder/integrations/ai-onboarding/", "/ai/integrations/agent-toolkit/"), + *[ + (route.path.replace("/ai/", "/ai-builder/", 1), route.path) + for route in routes + if route.path.startswith("/ai/") + ], + ("/ai/features/ide/", "/ai/features/editor-modes/"), + ("/ai-builder/features/ide/", "/ai/features/editor-modes/"), + ("/ai/features/customization/", "/ai/features/design-systems/"), + ("/ai-builder/features/customization/", "/ai/features/design-systems/"), + ("/hosting/adding-members/", "/hosting/project-members/"), + ("/hosting/projects/", "/hosting/project-members/"), + ("/authentication/authentication-overview/", "/enterprise/auth/overview/"), + ("/substates/overview/", "/state-structure/overview/"), + ("/substates/component-state/", "/state-structure/component-state/"), + ] diff --git a/docs/app/reflex_docs/reflex_docs.py b/docs/app/reflex_docs/reflex_docs.py index 2b0fde9d317..6674a732207 100644 --- a/docs/app/reflex_docs/reflex_docs.py +++ b/docs/app/reflex_docs/reflex_docs.py @@ -17,6 +17,7 @@ from reflex_site_shared.telemetry import get_pixel_website_trackers from reflex_docs.pages import page404, routes +from reflex_docs.redirects import get_redirects from reflex_docs.whitelist import _check_whitelisted_path # This number discovered by trial and error on Windows 11 w/ Node 18, any @@ -168,24 +169,7 @@ def _canonical_url(path: str) -> str: app.add_page(**page_args) # Add redirects. -redirects = [ - ("/ai/integrations/ai-onboarding/", "/ai/integrations/agent-toolkit/"), - ("/ai-builder/integrations/ai-onboarding/", "/ai/integrations/agent-toolkit/"), - *[ - (route.path.replace("/ai/", "/ai-builder/", 1), route.path) - for route in routes - if route.path.startswith("/ai/") - ], -] -redirects.extend([ - ("/ai/features/ide/", "/ai/features/editor-modes/"), - ("/ai-builder/features/ide/", "/ai/features/editor-modes/"), - ("/ai/features/customization/", "/ai/features/design-systems/"), - ("/ai-builder/features/customization/", "/ai/features/design-systems/"), - ("/hosting/adding-members/", "/hosting/project-members/"), - ("/hosting/projects/", "/hosting/project-members/"), - ("/authentication/authentication-overview/", "/enterprise/auth/overview/"), -]) +redirects = get_redirects(routes) def _redirect_page(): diff --git a/docs/app/reflex_docs/templates/docpage/sidebar/sidebar_items/learn.py b/docs/app/reflex_docs/templates/docpage/sidebar/sidebar_items/learn.py index 37704f3fa00..9326b977d4a 100644 --- a/docs/app/reflex_docs/templates/docpage/sidebar/sidebar_items/learn.py +++ b/docs/app/reflex_docs/templates/docpage/sidebar/sidebar_items/learn.py @@ -165,6 +165,7 @@ def get_sidebar_items_backend(): state_structure.component_state, state_structure.mixins, state_structure.shared_state, + state_structure.scaling_state, ], ), create_item( diff --git a/docs/app/tests/test_agent_files.py b/docs/app/tests/test_agent_files.py index c223060eeba..51242695643 100644 --- a/docs/app/tests/test_agent_files.py +++ b/docs/app/tests/test_agent_files.py @@ -15,6 +15,14 @@ markdown_path_for_trailing_slash_url, ) +AGENT_DIRECTIVE = ( + "> For AI agents: the complete documentation index is at " + "[llms.txt](https://reflex.dev/docs/llms.txt). Markdown versions are " + "available by appending `.md` or sending `Accept: text/markdown`. For " + "large or multi-page apps, start with [Scaling State]" + "(https://reflex.dev/docs/state-structure/scaling-state.md)." +) + def _patch_config(monkeypatch, deploy_url: str, frontend_path: str = "/docs"): """Patch the site config everywhere it is read. @@ -92,6 +100,15 @@ def test_generate_llms_txt_groups_docs_at_public_root(monkeypatch): "Use this index to find agent-readable Markdown docs, or see " "[llms-full.txt](https://reflex.dev/docs/llms-full.txt) for the " "complete docs in one file.\n\n" + "## Large App Architecture\n\n" + "For multi-page apps or apps with substantial state, read these guides " + "before choosing module and State boundaries:\n\n" + "1. [Scaling State]" + "(https://reflex.dev/docs/state-structure/scaling-state.md)\n" + "2. [Project Structure (Advanced)]" + "(https://reflex.dev/docs/advanced-onboarding/code-structure.md)\n" + "3. [State Structure]" + "(https://reflex.dev/docs/state-structure/overview.md)\n\n" "## Docs\n\n" ) assert "### Components\n\n" in content @@ -149,12 +166,7 @@ def test_generate_markdown_file_content_adds_agent_directive(monkeypatch, tmp_pa ) ) - assert content.startswith( - "> For AI agents: the complete documentation index is at " - "[llms.txt](https://reflex.dev/docs/llms.txt). Markdown versions are " - "available by appending `.md` or sending `Accept: text/markdown`.\n\n" - "# Overview" - ) + assert content.startswith(f"{AGENT_DIRECTIVE}\n\n# Overview") def test_generate_markdown_file_content_appends_component_props_table( @@ -218,10 +230,7 @@ def test_generate_dynamic_api_reference_files(monkeypatch): assert Path("api-reference/var.md") in files assert files[Path("api-reference/var.md")].startswith( - "> For AI agents: the complete documentation index is at " - "[llms.txt](https://reflex.dev/docs/llms.txt). Markdown versions are " - "available by appending `.md` or sending `Accept: text/markdown`.\n\n" - "# Var\n\n" + f"{AGENT_DIRECTIVE}\n\n# Var\n\n" ) assert "## Methods" in files[Path("api-reference/var.md")] assert "`reflex_base.vars.base.Var`" in files[Path("api-reference/var.md")] @@ -230,10 +239,7 @@ def test_generate_dynamic_api_reference_files(monkeypatch): # Dynamic API markdown files match the existing lowercase page routes. assert Path("api-reference/eventhandler.md") in files assert files[Path("api-reference/eventhandler.md")].startswith( - "> For AI agents: the complete documentation index is at " - "[llms.txt](https://reflex.dev/docs/llms.txt). Markdown versions are " - "available by appending `.md` or sending `Accept: text/markdown`.\n\n" - "# Eventhandler\n\n" + f"{AGENT_DIRECTIVE}\n\n# Eventhandler\n\n" ) assert Path("api-reference/event-handler.md") not in files assert Path("api-reference/componentstate.md") in files @@ -241,12 +247,7 @@ def test_generate_dynamic_api_reference_files(monkeypatch): assert Path("api-reference/importvar.md") in files env_vars = files[Path("api-reference/environment-variables.md")] - assert env_vars.startswith( - "> For AI agents: the complete documentation index is at " - "[llms.txt](https://reflex.dev/docs/llms.txt). Markdown versions are " - "available by appending `.md` or sending `Accept: text/markdown`.\n\n" - "# Environment Variables\n\n" - ) + assert env_vars.startswith(f"{AGENT_DIRECTIVE}\n\n# Environment Variables\n\n") assert "`reflex.config.EnvironmentVariables`" in env_vars # Dynamic API-reference pages must land in the llms.txt index. @@ -303,10 +304,7 @@ def test_generate_llms_full_txt_stitches_markdown_docs(monkeypatch, tmp_path): title="Eventhandler", section="API Reference", ), - "> For AI agents: the complete documentation index is at " - "[llms.txt](https://reflex.dev/docs/llms.txt). Markdown versions are " - "available by appending `.md` or sending `Accept: text/markdown`.\n\n" - "# Eventhandler\n\n" + f"{AGENT_DIRECTIVE}\n\n# Eventhandler\n\n" "`reflex_base.event.EventHandler`\n", ) ], @@ -319,6 +317,18 @@ def test_generate_llms_full_txt_stitches_markdown_docs(monkeypatch, tmp_path): "This file stitches together the full Reflex documentation as Markdown" ) assert "[llms.txt](https://reflex.dev/docs/llms.txt)" in content + assert ( + "[Scaling State](https://reflex.dev/docs/state-structure/scaling-state.md)" + in content + ) + assert ( + "[Project Structure (Advanced)]" + "(https://reflex.dev/docs/advanced-onboarding/code-structure.md)" in content + ) + assert ( + "[State Structure](https://reflex.dev/docs/state-structure/overview.md)" + in content + ) assert ( "# Introduction\n" "Source: https://reflex.dev/docs/getting-started/introduction.md\n\n" diff --git a/docs/app/tests/test_routes.py b/docs/app/tests/test_routes.py index b3454d2b775..ad12f4ec44d 100644 --- a/docs/app/tests/test_routes.py +++ b/docs/app/tests/test_routes.py @@ -64,6 +64,16 @@ def test_authentication_overview_moved_to_enterprise(routes_fixture): assert "/enterprise/auth/overview/" in paths +def test_legacy_substate_redirects(routes_fixture): + """Legacy substate routes redirect to their State Structure replacements.""" + from reflex_docs.redirects import get_redirects + + assert { + ("/substates/overview/", "/state-structure/overview/"), + ("/substates/component-state/", "/state-structure/component-state/"), + } <= set(get_redirects(routes_fixture)) + + def test_docs_route_descriptions_fit_search_snippet_length(routes_fixture): """Generated docs meta descriptions should not exceed the SEO snippet cap.""" overlong = { diff --git a/docs/app/tests/test_sidebar.py b/docs/app/tests/test_sidebar.py index ce7d0cef4bd..be1e1dc1c5b 100644 --- a/docs/app/tests/test_sidebar.py +++ b/docs/app/tests/test_sidebar.py @@ -20,3 +20,11 @@ def test_cross_reference_excluded_from_prev_next_chain(): prev, next_ = get_prev_next("/enterprise/auth/overview/") assert prev is not None and prev.link == "/enterprise/event-handler-api/" assert next_ is not None and next_.link == "/enterprise/auth/secure-by-default/" + + +def test_scaling_state_is_last_in_state_structure(): + """Scaling State closes the State Structure sidebar section.""" + from reflex_docs.templates.docpage.sidebar.sidebar_items.learn import backend + + state_structure = next(item for item in backend if item.names == "State Structure") + assert state_structure.children[-1].names == "Scaling State" diff --git a/docs/state_structure/component_state.md b/docs/state_structure/component_state.md index 56332e5849f..84cd40d62ed 100644 --- a/docs/state_structure/component_state.md +++ b/docs/state_structure/component_state.md @@ -12,8 +12,18 @@ instance of a component, rather than existing globally in the app. A Component S [Event Handlers](/docs/events/events-overview), and is useful for creating reusable components which operate independently of each other. +Use `ComponentState` when several explicitly created instances of one reusable widget need independent mutable State. +For page or workflow data, prefer a feature-owned class that directly inherits from `rx.State`. For reusable UI that +does not need independent mutable State, prefer a component function that accepts values and event handlers. + +See [Scaling State](/docs/state-structure/scaling-state#componentstate-owns-a-component-instance) for the complete +decision guide and the recommended pattern for dynamic collections. + ```md alert warning # ComponentState cannot be used inside `rx.foreach()` as it will only create one state instance for all elements in the loop. Each iteration of the foreach will share the same state, which may lead to unexpected behavior. + +Keep the selected item, editing identifier, or draft values in the owning page or feature State when rendering a +dynamic collection. Render each item with a stateless component function. ``` ## Using ComponentState diff --git a/docs/state_structure/mixins.md b/docs/state_structure/mixins.md index f6a546aff2d..4344d76b842 100644 --- a/docs/state_structure/mixins.md +++ b/docs/state_structure/mixins.md @@ -4,7 +4,15 @@ import reflex as rx # State Mixins -State mixins allow you to define shared functionality that can be reused across multiple State classes. This is useful for creating reusable components, shared business logic, or common state patterns. +State mixins allow you to define reactive functionality that can be reused across multiple State classes. This is useful when several concrete States need the same focused Vars, computed vars, or event handlers. + +A mixin reuses declarations; it does not create an independent State scope or one shared State instance. Every concrete +State that inherits the mixin owns its own resulting Vars. If several States need the same value, give that value one +State owner and access it with `get_var_value` or `get_state`. If reused logic does not need reactive Vars or event +handlers, prefer a plain helper or service. + +See [Scaling State](/docs/state-structure/scaling-state#mixins-reuse-behavior-not-state-instances) for guidance on +choosing between a mixin, helper, decentralized event handler, independent State, and inherited child State. ## What are State Mixins? @@ -216,6 +224,8 @@ This pattern allows you to build complex functionality by composing simpler mixi - **Document Dependencies**: If mixins depend on specific variables, document them - **Test Mixins**: Create test cases for mixin functionality - **Naming Convention**: Use descriptive names ending with "Mixin" +- **Prefer Plain Python First**: Use a helper or service unless the reused capability must declare Vars or event handlers +- **Avoid Aggregate States**: Composing many mixins into one concrete State can recreate a monolithic State and hide ownership ``` ## Limitations @@ -233,11 +243,13 @@ This pattern allows you to build complex functionality by composing simpler mixi State mixins are particularly useful for: -- **Form Validation**: Shared validation logic across forms +- **Form State**: Shared reactive validation and submission status +- **Pagination**: Common page Vars and navigation handlers - **UI State Management**: Common modal, loading, or notification patterns -- **Logging**: Centralized logging and debugging -- **API Integration**: Shared HTTP client functionality -- **Data Formatting**: Consistent data presentation across components +- **Data Formatting**: Computed vars used consistently by several concrete States + +Keep HTTP clients, repositories, logging, and business operations in plain Python services unless they must declare +reactive Vars or event handlers. A service is independently testable and does not add members to every consuming State. ```python demo exec import asyncio diff --git a/docs/state_structure/overview.md b/docs/state_structure/overview.md index e93cd09b134..2fa6931f3f2 100644 --- a/docs/state_structure/overview.md +++ b/docs/state_structure/overview.md @@ -9,6 +9,17 @@ Substates allow you to break up your state into multiple classes to make it more grows, as it allows you to think about each page as a separate entity. Substates also allow you to share common state resources, such as variables or event handlers. +This guide uses **substate** for any application State class below `rx.State` in the runtime State tree. There are two +important shapes: + +- A class that directly inherits from `rx.State` creates a flat, independently loaded branch and is the default for a + page or feature. +- A class that inherits from another application State creates a parent-child loading relationship. Use this only when + the child needs the parent for most of its events. + +See [Scaling State](/docs/state-structure/scaling-state) for the decision guide covering independent States, +inherited children, `ComponentState`, mixins, decentralized handlers, and `SharedState`. + When a particular state class becomes too large, breaking it up into several substates can bring performance benefits by only loading parts of the state that are used to handle a certain event. @@ -17,7 +28,7 @@ benefits by only loading parts of the state that are used to handle a certain ev One common pattern is to create a substate for each page in your app. This allows you to think about each page as a separate entity, and makes it easier to manage your code as your app grows. -To create a substate, simply inherit from `rx.State` multiple times: +To create independent State branches, define multiple classes that directly inherit from `rx.State`: ```python # index.py diff --git a/docs/state_structure/scaling_state.md b/docs/state_structure/scaling_state.md new file mode 100644 index 00000000000..7c41685a6ed --- /dev/null +++ b/docs/state_structure/scaling_state.md @@ -0,0 +1,295 @@ +# Scaling State + +Large Reflex applications are easiest to maintain when each piece of state has one clear owner. Prefer several flat, feature-owned State classes over one application-wide State or a deep inheritance tree, and use inheritance only when the parent and child must always be loaded together. + +Reflex State runs on the server and synchronizes changes to the browser. It is not a direct equivalent of React `useState` or Svelte component-local state. Choosing a State structure therefore affects ownership, loading, serialization, and how features depend on each other. See [How Reflex Works](/docs/advanced-onboarding/how-reflex-works) for the full runtime model. + +```md alert info +# Recommended default + +Organize a large application by feature. Give each page or workflow an independent State class that directly inherits from `rx.State`, keep application-wide State small, and access other State classes on demand instead of inheriting from them for convenience. +``` + +## Choose the Smallest Scope + +Use the following table before adding a Var or State class. + +| Need | Default | Why | +| --- | --- | --- | +| Reusable UI with no independently changing data | A component function with values and event handlers passed as arguments | Keeps shared UI independent of application State | +| Data and events owned by one page or feature | A class that directly inherits from `rx.State` | Creates a flat, independently loaded State branch | +| A small amount of per-user data used across features | A small application-defined session State | Gives cross-page data one owner without making all feature data global | +| One value from another State inside an event | `await self.get_var_value(OtherState.value)` | Makes the single-value dependency explicit and returns that value | +| Several values, or a mutation in another State | `await self.get_state(OtherState)` | Loads the other State explicitly and on demand | +| Several independently mutable instances of one reusable widget | [`rx.ComponentState`](/docs/state-structure/component-state) | Gives every statically created component instance its own State | +| Reusing ordinary Python or business logic | A helper, service, or repository function | Does not add Vars, inheritance, or loading relationships | +| Reusing Vars and event handlers across State classes | A narrow [State mixin](/docs/state-structure/mixins) | Copies reactive behavior into each concrete State; it does not create a shared State instance | +| Organizing many handlers without changing State ownership | [Decentralized event handlers](/docs/events/decentralized-event-handlers) | Splits code by feature while preserving the same State boundary | +| Parent data must be loaded with every child event | An inherited child State | Makes the runtime loading relationship explicit | +| Data intentionally synchronized across clients | [`rx.SharedState`](/docs/state-structure/shared-state) | Provides explicit cross-client synchronization | + +Persistent domain data normally belongs in a database, object store, or external service. State should hold the per-user projection needed by the UI, identifiers and filters for loading it, and the progress or errors for the current workflow. + +## Prefer Feature-Owned State + +For a small app, keeping the page, State, and components in one module is convenient. As a feature grows, split that module into a feature package instead of creating top-level packages containing every page, every State, and every event in the application. + +```text +support_app/ +├── support_app.py +├── core/ +│ ├── session.py +│ ├── permissions.py +│ └── database.py +├── features/ +│ ├── tickets/ +│ │ ├── page.py +│ │ ├── state.py +│ │ ├── events.py +│ │ ├── service.py +│ │ ├── components.py +│ │ └── tests/ +│ ├── customers/ +│ └── reports/ +└── shared/ + └── components/ +``` + +This layout keeps the code that changes together close together: + +- `state.py` owns the feature's Vars, computed vars, and small event handlers. +- `events.py` may contain decentralized handlers when the handler set becomes large. +- `service.py` contains database queries, external API calls, and business logic that does not need to be reactive. +- `components.py` binds feature-local UI to the feature State. +- `shared/components` contains reusable UI that accepts values and event handlers rather than importing a feature State. +- `core/session.py` owns only genuinely cross-feature per-user data, such as the current user, organization, or preferences. + +All modules containing decorated pages must still be imported by the application package so Reflex discovers them. See [Project Structure](/docs/advanced-onboarding/code-structure) for page discovery and application setup. + +### One Owner per Var + +Choose an owner based on which feature changes the value, not on how many pages display it. Any page may render a Var by importing its State class. An event handler that needs its runtime value should use `get_var_value` or `get_state`. + +```python +import reflex as rx + + +class PreferencesState(rx.State): + rows_per_page: int = 25 + + +class TicketsState(rx.State): + page: int = 0 + ticket_ids: list[int] = [] + + @rx.event + async def load_page(self): + rows_per_page = await self.get_var_value(PreferencesState.rows_per_page) + self.ticket_ids = await load_ticket_ids( + offset=self.page * rows_per_page, + limit=rows_per_page, + ) +``` + +`TicketsState` does not need to inherit from `PreferencesState`. The dependency exists only in the event that needs the preference, so unrelated ticket events do not need to load the preferences State. + +If an event needs several values or must update another State, load that State explicitly: + +```python +class ProfileState(rx.State): + display_name: str = "" + + +class OnboardingState(rx.State): + @rx.event + async def finish(self, display_name: str): + profile = await self.get_state(ProfileState) + profile.display_name = display_name +``` + +Cross-State mutation creates stronger coupling than a read. Keep it inside a clearly named workflow event and avoid using `get_state` as a general-purpose service locator. Shared domain operations usually belong in a service called by both States. + +## State Inheritance Is a Loading Decision + +Python inheritance may look like a convenient way to share methods, but State inheritance also creates a runtime State tree. When an event runs, Reflex loads the State containing the handler along with its parents and children. A large or highly connected parent can therefore make unrelated events more expensive. + +Use an inherited child State only when all of the following are true: + +1. The child logically specializes the parent. +2. The child needs the parent's data for most of its events. +3. Loading the parent and child together matches their intended lifetime. + +Otherwise, keep both classes directly under `rx.State` and access the other State on demand. See [State Structure](/docs/state-structure/overview) for the loading and computed-var implications. + +```python +# Prefer independent feature States for unrelated workflows. +class SearchState(rx.State): + query: str = "" + + +class CheckoutState(rx.State): + cart_id: str = "" + + +# Inherit only when the child is genuinely part of the same State tree. +class DocumentState(rx.State): + document_id: str = "" + + +class DocumentHistoryState(DocumentState): + revisions: list[str] = [] +``` + +## Mixins Reuse Behavior, Not State Instances + +A mixin contributes its Vars, computed vars, backend vars, and handlers to every concrete State that inherits it. Each concrete State owns its own resulting Vars. A mixin does not provide one shared value that several States read or update. + +Prefer a plain helper or service when the reused code does not need to declare reactive Vars or event handlers. Use a mixin when at least two concrete States need the same small, cohesive reactive capability, such as pagination behavior. + +```python +class PaginationMixin(rx.State, mixin=True): + page: int = 0 + page_size: int = 25 + + @rx.event + def next_page(self): + self.page += 1 + + @rx.event + def previous_page(self): + self.page = max(0, self.page - 1) + + +class TicketsState(PaginationMixin, rx.State): + ticket_ids: list[int] = [] + + +class CustomersState(PaginationMixin, rx.State): + customer_ids: list[int] = [] +``` + +Avoid a broad mixin that combines authentication, loading, errors, forms, database access, and feature data. Composing enough mixins into one concrete State recreates a monolithic State while hiding where each Var came from. Keep mixins shallow, document required members, and test each concrete consumer. See [State Mixins](/docs/state-structure/mixins) for syntax and limitations. + +## ComponentState Owns a Component Instance + +Use `rx.ComponentState` when a reusable widget is instantiated a known number of times and each instance must change independently. Examples include multiple editors, counters, or filter panels placed explicitly on a page. + +`ComponentState` is still server-side Reflex State. Use it for component-instance ownership, not to avoid a server event round trip for ephemeral browser interactions. + +Do not use `ComponentState` merely to shorten a page State, and do not use it for items produced by `rx.foreach`. `ComponentState` currently creates one shared instance for all iterations of a `foreach`, so repeated rows would affect each other. + +For a dynamic collection, keep the editing identity and drafts in the owning feature State and render each row as a stateless component: + +```python +class TicketsState(rx.State): + editing_ticket_id: int | None = None + title_drafts: dict[int, str] = {} + + @rx.event + def start_editing(self, ticket_id: int, title: str): + self.editing_ticket_id = ticket_id + self.title_drafts[ticket_id] = title + + @rx.event + def update_title_draft(self, ticket_id: int, title: str): + self.title_drafts[ticket_id] = title +``` + +The [Component State guide](/docs/state-structure/component-state) covers static instances, props, and access through the generated `.State` attribute. + +## Keep Shared Components Stateless + +A component used only inside one feature may import and bind directly to that feature's State. A component shared between features should normally receive the values and event handlers it needs. This keeps the shared component reusable and prevents feature import cycles. + +```python +class TicketSearchState(rx.State): + query: str = "" + + +def search_input(value, on_change) -> rx.Component: + return rx.input( + value=value, + on_change=on_change, + placeholder="Search", + ) + + +def ticket_toolbar() -> rx.Component: + return search_input(TicketSearchState.query, TicketSearchState.set_query) +``` + +Passing a State class itself through several component layers usually hides ownership. Pass the smallest value or event interface that the component needs. + +## Separate Schema from Large Handler Sets + +Keeping handlers on a State class is the simplest option and should remain the default while the class is readable. When one feature has many handlers, use decentralized event handlers to split them into the feature's `events.py` module without creating another State boundary. + +```python +class TicketsState(rx.State): + selected_ticket_id: int | None = None + + +@rx.event +def select_ticket(state: TicketsState, ticket_id: int): + state.selected_ticket_id = ticket_id +``` + +The type annotation makes the owning State explicit. Keep the handler in the same feature package as its State and page. + +## Refactor a Monolithic State + +Refactor in small, testable steps rather than rewriting the application around a new hierarchy. + +1. **Inventory ownership.** Group Vars and handlers by page, workflow, component instance, and cross-page session data. +2. **Extract non-reactive logic.** Move database queries, API clients, validation helpers, and domain operations into services. +3. **Create flat feature States.** Move one feature at a time to a class that directly inherits from `rx.State`. +4. **Replace convenience inheritance.** Use `get_var_value` for one cross-State value and `get_state` for explicit multi-value access or mutation. +5. **Extract reusable widgets carefully.** Use a component function first, then `ComponentState` only when instances need independent mutable State. +6. **Introduce mixins last.** Extract a mixin only after the same reactive capability exists in multiple concrete States. +7. **Verify routes and behavior.** Test page loading, navigation, multiple clients, background work, and every cross-State workflow after each extraction. + +Do not start by creating a shared `AppState` parent for every page. That makes access convenient but couples the State loading tree and provides an easy place for unrelated Vars to accumulate. + +## Test State Boundaries + +State classes are created and managed by Reflex. Do not construct them directly in application tests. Keep most domain logic in plain functions and services that can be unit tested without the Reflex runtime, then test State wiring through the application. + +For every new State boundary, cover the behavior that made the boundary necessary: + +- Open the same workflow as two clients and verify their private State remains isolated. +- Navigate away from and back to a page and verify its intended reset or persistence behavior. +- Exercise `on_load`, async events, and [background events](/docs/events/background-events) through their real event triggers. +- Verify that a cross-State event changes only the intended owner and that the consuming UI updates. +- Create multiple `ComponentState` instances and verify their values remain independent. +- Test every concrete State that consumes a mixin, including member-name conflicts and required members. +- Compile an application that imports every decorated page so missing page registrations and circular imports fail in CI. + +When refactoring a monolithic State, keep the existing end-to-end tests passing after each extraction. Add a regression test for the new ownership boundary before removing the old Var or handler. + +## Performance Checklist + +- Keep most feature States as direct subclasses of `rx.State`. +- Put only data rendered by the browser in frontend Vars. Use [backend-only Vars](/docs/vars/base-vars#backend-only-vars) for per-session server data that should not be synchronized. +- Store persistent records in the database and synchronize only the UI projection needed by the current page. +- Use `get_var_value` when an event needs one value from a large State. +- Keep computed vars out of broad ancestor States unless all descendants depend on them. +- Keep `SharedState` minimal because updates may affect many linked clients. +- Use stable identifiers rather than duplicating large objects across several State classes. +- Measure event latency and synchronized payloads when changing a State boundary; a smaller source file does not necessarily mean a smaller runtime State. + +## Review Checklist + +Before adding or moving State, answer these questions: + +1. Which feature owns the value? +2. Is the value persistent domain data, per-user UI State, component-instance State, or cross-client State? +3. What creates and resets it? +4. Which events may mutate it? +5. Does another State need one value, the full State, or only a shared service operation? +6. Would inheritance make unrelated events load this data? +7. Would a helper or decentralized handler solve the organization problem without changing State ownership? +8. Is a mixin sharing behavior, or accidentally hiding a monolith? +9. Is `ComponentState` being used inside `rx.foreach`? +10. Can a shared component accept values and events instead of importing feature State? + +If the answers are unclear, keep the State boundary local to the feature until a concrete sharing requirement appears.