[REF-1349] RechartsCharts and ResponsiveContainer must be memo leaf (#2240)

This commit is contained in:
Masen Furer 2023-12-01 09:49:59 -08:00 committed by GitHub
parent 3e9e718b89
commit 4ada79c1e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 145 additions and 35 deletions

View File

@ -28,6 +28,7 @@ from reflex.constants import (
EventTriggers, EventTriggers,
Hooks, Hooks,
Imports, Imports,
MemoizationDisposition,
MemoizationMode, MemoizationMode,
PageNames, PageNames,
) )
@ -1363,20 +1364,29 @@ class StatefulComponent(BaseComponent):
""" """
from reflex.components.layout.foreach import Foreach from reflex.components.layout.foreach import Foreach
if component._memoization_mode.disposition == MemoizationDisposition.NEVER:
# Never memoize this component.
return None
if component.tag is None: if component.tag is None:
# Only memoize components with a tag. # Only memoize components with a tag.
return None return None
# If _var_data is found in this component, it is a candidate for auto-memoization. # If _var_data is found in this component, it is a candidate for auto-memoization.
has_var_data = False should_memoize = False
# Determine if any Vars have associated data. # If the component requests to be memoized, then ignore other checks.
for prop_var in component._get_vars(): if component._memoization_mode.disposition == MemoizationDisposition.ALWAYS:
if prop_var._var_data: should_memoize = True
has_var_data = True
break
if not has_var_data: if not should_memoize:
# Determine if any Vars have associated data.
for prop_var in component._get_vars():
if prop_var._var_data:
should_memoize = True
break
if not should_memoize:
# Check for special-cases in child components. # Check for special-cases in child components.
for child in component.children: for child in component.children:
# Skip BaseComponent and StatefulComponent children. # Skip BaseComponent and StatefulComponent children.
@ -1384,14 +1394,14 @@ class StatefulComponent(BaseComponent):
continue continue
# Always consider Foreach something that must be memoized by the parent. # Always consider Foreach something that must be memoized by the parent.
if isinstance(child, Foreach): if isinstance(child, Foreach):
has_var_data = True should_memoize = True
break break
child = cls._child_var(child) child = cls._child_var(child)
if isinstance(child, Var) and child._var_data: if isinstance(child, Var) and child._var_data:
has_var_data = True should_memoize = True
break break
if has_var_data or component.event_triggers: if should_memoize or component.event_triggers:
# Render the component to determine tag+hash based on component code. # Render the component to determine tag+hash based on component code.
tag_name = cls._get_tag_name(component) tag_name = cls._get_tag_name(component)
if tag_name is None: if tag_name is None:

View File

@ -770,10 +770,10 @@ class FunnelChart(RechartsCharts):
] = None, ] = None,
**props **props
) -> "FunnelChart": ) -> "FunnelChart":
"""Create the component. """Create a Recharts chart container component (mixin).
Args: Args:
*children: The children of the component. *children: The children components.
data: The source data, in which each element is an object. data: The source data, in which each element is an object.
sync_id: If any two categorical charts(rx.line_chart, rx.area_chart, rx.bar_chart, rx.composed_chart) have the same sync_id, these two charts can sync the position GraphingTooltip, and the start_index, end_index of Brush. sync_id: If any two categorical charts(rx.line_chart, rx.area_chart, rx.bar_chart, rx.composed_chart) have the same sync_id, these two charts can sync the position GraphingTooltip, and the start_index, end_index of Brush.
sync_method: When sync_id is provided, allows customisation of how the charts will synchronize GraphingTooltips and brushes. Using 'index' (default setting), other charts will reuse current datum's index within the data array. In cases where data does not have the same length, this might yield unexpected results. In that case use 'value' which will try to match other charts values, or a fully custom function which will receive tick, data as argument and should return an index. 'index' | 'value' | function sync_method: When sync_id is provided, allows customisation of how the charts will synchronize GraphingTooltips and brushes. Using 'index' (default setting), other charts will reuse current datum's index within the data array. In cases where data does not have the same length, this might yield unexpected results. In that case use 'value' which will try to match other charts values, or a fully custom function which will receive tick, data as argument and should return an index. 'index' | 'value' | function
@ -791,10 +791,7 @@ class FunnelChart(RechartsCharts):
**props: The props of the component. **props: The props of the component.
Returns: Returns:
The component. A Recharts component.
Raises:
TypeError: If an invalid child is passed.
""" """
... ...

View File

@ -13,10 +13,11 @@ from .recharts import (
LiteralPosition, LiteralPosition,
LiteralVerticalAlign, LiteralVerticalAlign,
Recharts, Recharts,
RechartsMemoizationLeafMixin,
) )
class ResponsiveContainer(Recharts): class ResponsiveContainer(Recharts, RechartsMemoizationLeafMixin):
"""A base class for responsive containers in Recharts.""" """A base class for responsive containers in Recharts."""
tag = "ResponsiveContainer" tag = "ResponsiveContainer"

View File

@ -17,9 +17,10 @@ from .recharts import (
LiteralPosition, LiteralPosition,
LiteralVerticalAlign, LiteralVerticalAlign,
Recharts, Recharts,
RechartsMemoizationLeafMixin,
) )
class ResponsiveContainer(Recharts): class ResponsiveContainer(Recharts, RechartsMemoizationLeafMixin):
@overload @overload
@classmethod @classmethod
def create( # type: ignore def create( # type: ignore
@ -84,10 +85,10 @@ class ResponsiveContainer(Recharts):
] = None, ] = None,
**props **props
) -> "ResponsiveContainer": ) -> "ResponsiveContainer":
"""Create the component. """Create a Recharts chart container component (mixin).
Args: Args:
*children: The children of the component. *children: The children components.
aspect: The aspect ratio of the container. The final aspect ratio of the SVG element will be (width / height) * aspect. Number aspect: The aspect ratio of the container. The final aspect ratio of the SVG element will be (width / height) * aspect. Number
width: The width of chart container. Can be a number or string width: The width of chart container. Can be a number or string
height: The height of chart container. Number height: The height of chart container. Number
@ -103,10 +104,7 @@ class ResponsiveContainer(Recharts):
**props: The props of the component. **props: The props of the component.
Returns: Returns:
The component. A Recharts component.
Raises:
TypeError: If an invalid child is passed.
""" """
... ...

View File

@ -2,16 +2,45 @@
from typing import Literal from typing import Literal
from reflex.components.component import Component, NoSSRComponent from reflex.components.component import Component, NoSSRComponent
from reflex.constants import MemoizationDisposition, MemoizationMode
class Recharts(Component): class Recharts(Component):
"""A component that wraps a victory lib.""" """A component that wraps a recharts lib."""
library = "recharts@2.8.0" library = "recharts@2.8.0"
class RechartsCharts(NoSSRComponent): class RechartsMemoizationLeafMixin(Component):
"""A component that wraps a victory lib.""" """A mixin for Recharts components that must not memoize their children separately.
This includes all chart types and ResponsiveContainer itself.
"""
_memoization_mode = MemoizationMode(recursive=False)
@classmethod
def create(cls, *children, **props) -> Component:
"""Create a Recharts chart container component (mixin).
Args:
*children: The children components.
**props: The props of the component.
Returns:
A Recharts component.
"""
comp = super().create(*children, **props)
if comp.get_hooks():
# If any of the children depend on state, then this instance needs to memoize.
comp._memoization_mode = cls._memoization_mode.copy(
update={"disposition": MemoizationDisposition.ALWAYS},
)
return comp
class RechartsCharts(NoSSRComponent, RechartsMemoizationLeafMixin):
"""A component that wraps a recharts lib."""
library = "recharts@2.8.0" library = "recharts@2.8.0"

View File

@ -9,6 +9,7 @@ from reflex.event import EventChain, EventHandler, EventSpec
from reflex.style import Style from reflex.style import Style
from typing import Literal from typing import Literal
from reflex.components.component import Component, NoSSRComponent from reflex.components.component import Component, NoSSRComponent
from reflex.constants import MemoizationDisposition, MemoizationMode
class Recharts(Component): class Recharts(Component):
@overload @overload
@ -89,7 +90,83 @@ class Recharts(Component):
""" """
... ...
class RechartsCharts(NoSSRComponent): class RechartsMemoizationLeafMixin(Component):
@overload
@classmethod
def create( # type: ignore
cls,
*children,
style: Optional[Style] = None,
key: Optional[Any] = None,
id: Optional[Any] = None,
class_name: Optional[Any] = None,
autofocus: Optional[bool] = None,
custom_attrs: Optional[Dict[str, Union[Var, str]]] = None,
on_blur: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_click: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_context_menu: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_double_click: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_focus: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_mount: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_mouse_down: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_mouse_enter: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_mouse_leave: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_mouse_move: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_mouse_out: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_mouse_over: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_mouse_up: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_scroll: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_unmount: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
**props
) -> "RechartsMemoizationLeafMixin":
"""Create a Recharts chart container component (mixin).
Args:
*children: The children components.
style: The style of the component.
key: A unique key for the component.
id: The id for the component.
class_name: The class name for the component.
autofocus: Whether the component should take the focus once the page is loaded
custom_attrs: custom attribute
**props: The props of the component.
Returns:
A Recharts component.
"""
...
class RechartsCharts(NoSSRComponent, RechartsMemoizationLeafMixin):
@overload @overload
@classmethod @classmethod
def create( # type: ignore def create( # type: ignore
@ -148,10 +225,10 @@ class RechartsCharts(NoSSRComponent):
] = None, ] = None,
**props **props
) -> "RechartsCharts": ) -> "RechartsCharts":
"""Create the component. """Create a Recharts chart container component (mixin).
Args: Args:
*children: The children of the component. *children: The children components.
style: The style of the component. style: The style of the component.
key: A unique key for the component. key: A unique key for the component.
id: The id for the component. id: The id for the component.
@ -161,10 +238,7 @@ class RechartsCharts(NoSSRComponent):
**props: The props of the component. **props: The props of the component.
Returns: Returns:
The component. A Recharts component.
Raises:
TypeError: If an invalid child is passed.
""" """
... ...

View File

@ -115,7 +115,8 @@ class MemoizationDisposition(enum.Enum):
# If the component uses state or events, it should be memoized. # If the component uses state or events, it should be memoized.
STATEFUL = "stateful" STATEFUL = "stateful"
# TODO: add more modes, like always and never ALWAYS = "always"
NEVER = "never"
class MemoizationMode(Base): class MemoizationMode(Base):