Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions reflex/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,15 @@ class PerformanceMode(enum.Enum):
OFF = "off"


@enum.unique
class StateMinifyMode(enum.Enum):
"""Mode for state name minification."""

DISABLED = "disabled" # Never minify state names (default)
ENABLED = "enabled" # Minify states that have explicit state_id
ENFORCE = "enforce" # Require all non-mixin states to have state_id


class ExecutorType(enum.Enum):
"""Executor for compiling the frontend."""

Expand Down Expand Up @@ -688,6 +697,9 @@ class EnvironmentVariables:
# The maximum size of the reflex state in kilobytes.
REFLEX_STATE_SIZE_LIMIT: EnvVar[int] = env_var(1000)

# State name minification mode: disabled, enabled, or enforce.
REFLEX_MINIFY_STATES: EnvVar[StateMinifyMode] = env_var(StateMinifyMode.DISABLED)

# Whether to use the turbopack bundler.
REFLEX_USE_TURBOPACK: EnvVar[bool] = env_var(False)

Expand Down
93 changes: 89 additions & 4 deletions reflex/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,34 @@
# For BaseState.get_var_value
VAR_TYPE = TypeVar("VAR_TYPE")

# Global registry: state_id -> state class (for duplicate detection)
_state_id_registry: dict[int, type[BaseState]] = {}


def _int_to_minified_name(state_id: int) -> str:
"""Convert integer state_id to minified name using base-54 encoding.

Args:
state_id: The integer state ID to convert.

Returns:
The minified state name (e.g., 0->'a', 1->'b', 54->'ba').
"""
# All possible chars for minified state name (valid JS identifiers)
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_"
base = len(chars)

if state_id == 0:
return chars[0]

name = ""
num = state_id
while num > 0:
name = chars[num % base] + name
num //= base

return name


def _no_chain_background_task(state: BaseState, name: str, fn: Callable) -> Callable:
"""Protect against directly chaining a background task from another event handler.
Expand Down Expand Up @@ -391,6 +419,9 @@ class BaseState(EvenMoreBasicBaseState):
# Set of states which might need to be recomputed if vars in this state change.
_potentially_dirty_states: ClassVar[set[str]] = set()

# The explicit state ID for minification (None = use full name).
_state_id: ClassVar[int | None] = None

# The parent state.
parent_state: BaseState | None = field(default=None, is_var=False)

Expand Down Expand Up @@ -508,23 +539,52 @@ def _validate_module_name(cls) -> None:
raise NameError(msg)

@classmethod
def __init_subclass__(cls, mixin: bool = False, **kwargs):
def __init_subclass__(
cls, mixin: bool = False, state_id: int | None = None, **kwargs
):
"""Do some magic for the subclass initialization.

Args:
mixin: Whether the subclass is a mixin and should not be initialized.
state_id: Explicit state ID for minified state names.
**kwargs: The kwargs to pass to the init_subclass method.

Raises:
StateValueError: If a substate class shadows another.
StateValueError: If a substate class shadows another or duplicate state_id.
"""
from reflex.utils.exceptions import StateValueError

super().__init_subclass__(**kwargs)

# Mixin states cannot have state_id
if cls._mixin:
if state_id is not None:
msg = (
f"Mixin state '{cls.__module__}.{cls.__name__}' cannot have a state_id. "
"Remove state_id or mixin=True."
)
raise StateValueError(msg)
return

# Store state_id as class variable (only for non-mixins)
cls._state_id = state_id

# Validate state_id if provided (check for duplicates)
if state_id is not None:
if state_id in _state_id_registry:
existing_cls = _state_id_registry[state_id]
# Allow re-registration if it's the same class (e.g., module reload)
existing_key = f"{existing_cls.__module__}.{existing_cls.__name__}"
new_key = f"{cls.__module__}.{cls.__name__}"
if existing_key != new_key:
msg = (
f"Duplicate state_id={state_id}. Already used by "
f"'{existing_cls.__module__}.{existing_cls.__name__}', "
f"cannot be reused by '{cls.__module__}.{cls.__name__}'."
)
raise StateValueError(msg)
_state_id_registry[state_id] = cls

# Handle locally-defined states for pickling.
if "<locals>" in cls.__qualname__:
cls._handle_local_def()
Expand Down Expand Up @@ -988,9 +1048,34 @@ def get_name(cls) -> str:

Returns:
The name of the state.

Raises:
StateValueError: If ENFORCE mode is set and state_id is missing.
"""
from reflex.environment import StateMinifyMode
from reflex.utils.exceptions import StateValueError

module = cls.__module__.replace(".", "___")
return format.to_snake_case(f"{module}___{cls.__name__}")
full_name = format.to_snake_case(f"{module}___{cls.__name__}")

minify_mode = environment.REFLEX_MINIFY_STATES.get()

if minify_mode == StateMinifyMode.DISABLED:
return full_name

if cls._state_id is not None:
return _int_to_minified_name(cls._state_id)

# state_id not set
if minify_mode == StateMinifyMode.ENFORCE:
msg = (
f"State '{cls.__module__}.{cls.__name__}' is missing required state_id. "
f"Add state_id parameter: class {cls.__name__}(rx.State, state_id=N)"
)
raise StateValueError(msg)

# ENABLED mode with no state_id - use full name
return full_name

@classmethod
@functools.lru_cache
Expand Down Expand Up @@ -2464,7 +2549,7 @@ def is_serializable(value: Any) -> bool:
T_STATE = TypeVar("T_STATE", bound=BaseState)


class State(BaseState):
class State(BaseState, state_id=0):
"""The app Base State."""

# The hydrated bool.
Expand Down
5 changes: 5 additions & 0 deletions reflex/vars/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3540,6 +3540,7 @@ def __new__(
bases: tuple[type],
namespace: dict[str, Any],
mixin: bool = False,
state_id: int | None = None,
) -> type:
"""Create a new class.

Expand All @@ -3548,6 +3549,7 @@ def __new__(
bases: The bases of the class.
namespace: The namespace of the class.
mixin: Whether the class is a mixin and should not be instantiated.
state_id: Explicit state ID for minified state names.

Returns:
The new class.
Expand Down Expand Up @@ -3648,6 +3650,9 @@ def __new__(
namespace["__inherited_fields__"] = inherited_fields
namespace["__fields__"] = inherited_fields | own_fields
namespace["_mixin"] = mixin
# Pass state_id to __init_subclass__ if provided (for BaseState subclasses)
if state_id is not None:
return super().__new__(cls, name, bases, namespace, state_id=state_id)
return super().__new__(cls, name, bases, namespace)


Expand Down
Loading
Loading