Skip to content

Compiler: Add module_code field to VarData #6219

@masenf

Description

@masenf

Summary

Add a module_code field to VarData so that Vars can declare custom JavaScript code that needs to be present at the module level of the page (or component file) where the Var is rendered. The compiler will pick up module_code from Vars via the components those Vars are attached to.

This is the Var-level equivalent of Component._get_custom_code(), but with a clearer name. The long-term goal is to standardize on module_code throughout the codebase and eventually deprecate the custom_code naming.

Background

Current state: custom_code on Component

Today, custom JavaScript snippets that need to be at the module level are handled via Component._get_custom_code():

class MyComponent(rx.Component):
    def _get_custom_code(self) -> str | None:
        return "console.log('This runs at module level');"

These are collected recursively via _get_all_custom_code() and passed to the page template as custom_codes=....

What's missing: Vars can't contribute module code

VarData currently has these fields:

@dataclasses.dataclass(eq=True, frozen=True)
class VarData:
    state: str = ""
    field_name: str = ""
    imports: ParsedImportTuple = ()
    hooks: tuple[str, ...] = ()
    deps: tuple[Var, ...] = ()
    position: Hooks.HookPosition | None = None
    components: tuple[BaseComponent, ...] = ()

Vars can declare imports and hooks, but they cannot declare module-level code. This means if a Var needs a module-level helper function or constant, it must be handled indirectly through a Component — even when the Var itself is the thing that needs it.

Proposed Change

Add a module_code field to VarData:

@dataclasses.dataclass(eq=True, frozen=True)
class VarData:
    state: str = ""
    field_name: str = ""
    imports: ParsedImportTuple = ()
    hooks: tuple[str, ...] = ()
    deps: tuple[Var, ...] = ()
    position: Hooks.HookPosition | None = None
    components: tuple[BaseComponent, ...] = ()
    module_code: tuple[str, ...] = ()  # NEW

Compiler integration

The compiler (or ConsolidateModuleCodePlugin in the new single-pass architecture) should collect module_code from Vars during compilation. Since Vars are attached to components (via props, event handlers, children), the component tree walk already visits them. The collection path is:

  1. For each component, examine its Var-containing fields (props, event handlers, style, children that are Vars)
  2. For each Var, check _var_data.module_code
  3. Add any non-empty module code to the page's module code set

This naturally integrates with the existing _get_all_custom_code() flow (or its plugin replacement). The module code from Vars and from Components should be merged into the same output set — they both end up as top-level JS in the page file.

VarData.merge() update

The VarData.merge() static method needs to handle the new field:

@staticmethod
def merge(*others: VarData | None) -> VarData | None:
    ...
    module_code = tuple({mc for od in others if od for mc in od.module_code})
    ...

Naming: module_code vs custom_code

The existing codebase uses custom_code for the same concept on Components. This name is confusing because "custom code" could mean many things. module_code is more precise — it's code that goes at the JS module level (top of the file, outside any component function).

This issue introduces module_code on VarData. A follow-up task (not in scope here) would be to rename _get_custom_code() / _get_all_custom_code() on Component to _get_module_code() / _get_all_module_code() and standardize the naming throughout.

Acceptance Criteria

  • VarData has a module_code: tuple[str, ...] field (default empty tuple)
  • VarData.merge() correctly combines module_code from multiple VarData instances (deduplicating)
  • VarData.__bool__ / truthiness accounts for the new field
  • The compiler collects module_code from Vars encountered during page compilation
  • Module code from Vars appears in the compiled page output alongside Component custom code
  • Unit test: a Var with module_code attached to a component results in that code appearing in the compiled page
  • All existing tests pass

Key Files

  • reflex/vars/base.py:
    • VarData dataclass (~line 99) — add the field
    • VarData.merge() — update merging logic
  • reflex/components/component.py:
    • _get_all_custom_code() (~line 1590) — should also collect from Vars (or a new method should)
    • _get_vars() / _get_vars_from_props() — how Vars are currently extracted from components
  • reflex/compiler/compiler.py:
    • _compile_page() — where custom_codes are passed to the template
  • reflex/compiler/templates.py:
    • page_template() — receives custom_codes parameter

Notes

  • The module_code tuple uses strings (like hooks), deduplicated by set membership. Order shouldn't matter for module-level code.
  • This is a prerequisite for the app_wraps-on-VarData work, where Vars need to declare providers and context code at the app level.
  • The frozen dataclass pattern is maintained — module_code is a tuple, not a list.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementAnything you want improved

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions