diff --git a/reflex/components/component.py b/reflex/components/component.py index 8af5d832788..438d9404367 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -5,6 +5,7 @@ import contextlib import copy import dataclasses +import enum import functools import inspect import typing @@ -479,6 +480,57 @@ def _components_from( return () +def _deterministic_hash(value: object) -> int: + """Hash a rendered dictionary. + + Args: + value: The dictionary to hash. + + Returns: + The hash of the dictionary. + + Raises: + TypeError: If the value is not hashable. + """ + if isinstance(value, BaseComponent): + # If the value is a component, hash its rendered code. + rendered_code = value.render() + return _deterministic_hash(rendered_code) + if isinstance(value, Var): + return _deterministic_hash((value._js_expr, value._get_all_var_data())) + if isinstance(value, VarData): + return _deterministic_hash(dataclasses.asdict(value)) + if isinstance(value, dict): + # Sort the dictionary to ensure consistent hashing. + return _deterministic_hash( + tuple(sorted((k, _deterministic_hash(v)) for k, v in value.items())) + ) + if isinstance(value, int): + # Hash numbers and booleans directly. + return int(value) + if isinstance(value, float): + return _deterministic_hash(str(value)) + if isinstance(value, str): + return int(md5(f'"{value}"'.encode()).hexdigest(), 16) + if isinstance(value, (tuple, list)): + # Hash tuples by hashing each element. + return _deterministic_hash( + "[" + ",".join(map(str, map(_deterministic_hash, value))) + "]" + ) + if isinstance(value, enum.Enum): + # Hash enums by their name. + return _deterministic_hash(str(value)) + if value is None: + # Hash None as a special case. + return _deterministic_hash("None") + + msg = ( + f"Cannot hash value `{value}` of type `{type(value).__name__}`. " + "Only BaseComponent, Var, VarData, dict, str, tuple, and enum.Enum are supported." + ) + raise TypeError(msg) + + DEFAULT_TRIGGERS: Mapping[str, types.ArgsSpec | Sequence[types.ArgsSpec]] = { EventTriggers.ON_FOCUS: no_args_event_spec, EventTriggers.ON_BLUR: no_args_event_spec, @@ -2430,7 +2482,7 @@ def _get_tag_name(cls, component: Component) -> str | None: return None # Compute the hash based on the rendered code. - code_hash = md5(str(rendered_code).encode("utf-8")).hexdigest() + code_hash = _deterministic_hash(rendered_code) # Format the tag name including the hash. return format.format_state_name(