From fe37d2c1b81ec9c13753812c2db213b2702db5bd Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 17 Aug 2023 12:06:52 -0700 Subject: [PATCH 01/11] implement on_mount and on_unmount for all components with useEffect --- reflex/.templates/web/utils/state.js | 5 +++++ reflex/components/component.py | 31 +++++++++++++++++++++++++--- reflex/constants.py | 3 +++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index 5c382a3cf3..292dfa3c5c 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -218,6 +218,11 @@ export const queueEvents = async (events, socket) => { export const processEvent = async ( socket ) => { + // Only proceed if the socket is up, otherwise we throw the event into the void + if (!socket) { + return; + } + // Only proceed if we're not already processing an event. if (event_queue.length === 0 || event_processing) { return; diff --git a/reflex/components/component.py b/reflex/components/component.py index 0f4756fd0e..128afd4af4 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -279,7 +279,7 @@ def get_triggers(self) -> Set[str]: Returns: The event triggers. """ - return EVENT_TRIGGERS | set(self.get_controlled_triggers()) + return EVENT_TRIGGERS | set(self.get_controlled_triggers()) | set((constants.ON_MOUNT, constants.ON_UNMOUNT)) def get_controlled_triggers(self) -> Dict[str, Var]: """Get the event triggers that pass the component's value to the handler. @@ -509,10 +509,35 @@ def _get_hooks(self) -> Optional[str]: Returns: The hooks for just this component. """ + hooks = [] ref = self.get_ref() if ref is not None: - return f"const {ref} = useRef(null); refs['{ref}'] = {ref};" - return None + hooks.append(f"const {ref} = useRef(null); refs['{ref}'] = {ref};") + # pop on_mount and on_unmount from event_triggers for useEffect + on_mount = self.event_triggers.pop(constants.ON_MOUNT, "") + on_unmount = self.event_triggers.pop(constants.ON_UNMOUNT, "") + if on_mount or on_unmount: + if on_mount: + on_mount = "Event([{chain}])".format( + chain=",".join( + [format.format_event(event) for event in on_mount.events] + ) + ) + if on_unmount: + on_unmount = "Event([{chain}])".format( + chain=",".join( + [format.format_event(event) for event in on_unmount.events] + ) + ) + hooks.append(f""" + useEffect(() => {{ + {on_mount} + return () => {{ + {on_unmount} + }} + }}, []);""") + hooks.append('console.log("state.hello_mounted", state.hello_mounted)') + return "\n".join(hooks) if hooks else None def get_hooks(self) -> Set[str]: """Get the React hooks for this component and its children. diff --git a/reflex/constants.py b/reflex/constants.py index 98727da515..bed50e6b51 100644 --- a/reflex/constants.py +++ b/reflex/constants.py @@ -347,3 +347,6 @@ class RouteRegex(SimpleNamespace): # Alembic migrations ALEMBIC_CONFIG = os.environ.get("ALEMBIC_CONFIG", "alembic.ini") + +ON_MOUNT = "on_mount" +ON_UNMOUNT = "on_unmount" From 596d6718c416275833bbe9add133245879210ac2 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 18 Aug 2023 09:20:22 -0700 Subject: [PATCH 02/11] format: add format_var and format_event_chain --- reflex/utils/format.py | 56 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/reflex/utils/format.py b/reflex/utils/format.py index 7eef1c194b..759dc092db 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -14,7 +14,7 @@ import plotly.graph_objects as go from plotly.io import to_json -from reflex import constants +from reflex import constants, vars from reflex.utils import types if TYPE_CHECKING: @@ -182,6 +182,24 @@ def format_string(string: str) -> str: return string +def format_var(var: vars.Var) -> str: + """Format the given Var as a javascript value. + + Args: + var: The Var to format. + + Returns: + The formatted Var. + """ + if not var.is_local or var.is_string: + return str(var) + if types._issubclass(var.type_, str): + return format_string(var.full_name) + if format.is_wrapped(var.full_name, "{"): + return var.full_name + return format.json_dumps(var.full_name) + + def format_route(route: str) -> str: """Format the given route. @@ -311,6 +329,42 @@ def format_event(event_spec: EventSpec) -> str: return f"E({', '.join(event_args)})" +def format_event_chain( + event_chain: [EventChain | vars.Var[EventChain]], + event_arg: vars.Var | None = None, +) -> str: + """Format an event chain as a javascript invocation. + + Args: + event_chain: The event chain to queue on the frontend. + event_arg: The browser-native event (only used to preventDefault). + + Returns: + Compiled javascript code to queue the given event chain on the frontend. + + Raises: + ValueError: When the given event chain is not a valid event chain. + """ + if isinstance(event_chain, vars.Var): + if event_chain.type_ is not EventChain: + raise ValueError(f"Invalid event chain: {event_chain}") + return "".join( + "(() => {", + format_var(event_chain), + f"; preventDefault({format_var(event_arg)})" if event_arg else "", + "})()", + ) + + chain = ",".join([format_event(event) for event in event_chain.events]) + return "".join( + [ + f"Event([{chain}]", + f", {format_var(event_arg)}" if event_arg else "", + ")", + ] + ) + + def format_query_params(router_data: Dict[str, Any]) -> Dict[str, str]: """Convert back query params name to python-friendly case. From 144b5a940fec9dcf7f9679f4b71f38cbf1b6b5c1 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 18 Aug 2023 09:21:12 -0700 Subject: [PATCH 03/11] component: split hooks into user and internal --- reflex/components/component.py | 78 ++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 26 deletions(-) diff --git a/reflex/components/component.py b/reflex/components/component.py index 128afd4af4..30ebf404ef 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -279,7 +279,11 @@ def get_triggers(self) -> Set[str]: Returns: The event triggers. """ - return EVENT_TRIGGERS | set(self.get_controlled_triggers()) | set((constants.ON_MOUNT, constants.ON_UNMOUNT)) + return ( + EVENT_TRIGGERS + | set(self.get_controlled_triggers()) + | set((constants.ON_MOUNT, constants.ON_UNMOUNT)) + ) def get_controlled_triggers(self) -> Dict[str, Var]: """Get the event triggers that pass the component's value to the handler. @@ -503,41 +507,63 @@ def get_imports(self) -> imports.ImportDict: self._get_imports(), *[child.get_imports() for child in self.children] ) - def _get_hooks(self) -> Optional[str]: - """Get the React hooks for this component. + def _get_mount_lifecycle_hook(self) -> str | None: + """Generate the component lifecycle hook. Returns: - The hooks for just this component. + The useEffect hook for managing `on_mount` and `on_unmount` events. """ - hooks = [] - ref = self.get_ref() - if ref is not None: - hooks.append(f"const {ref} = useRef(null); refs['{ref}'] = {ref};") - # pop on_mount and on_unmount from event_triggers for useEffect + # pop on_mount and on_unmount from event_triggers since these are handled by + # hooks, not as actually props in the component on_mount = self.event_triggers.pop(constants.ON_MOUNT, "") on_unmount = self.event_triggers.pop(constants.ON_UNMOUNT, "") + if on_mount: + on_mount = format.format_event_chain(on_mount) + if on_unmount: + on_unmount = format.format_event_chain(on_unmount) if on_mount or on_unmount: - if on_mount: - on_mount = "Event([{chain}])".format( - chain=",".join( - [format.format_event(event) for event in on_mount.events] - ) - ) - if on_unmount: - on_unmount = "Event([{chain}])".format( - chain=",".join( - [format.format_event(event) for event in on_unmount.events] - ) - ) - hooks.append(f""" + return f""" useEffect(() => {{ {on_mount} return () => {{ {on_unmount} }} - }}, []);""") - hooks.append('console.log("state.hello_mounted", state.hello_mounted)') - return "\n".join(hooks) if hooks else None + }}, []);""" + + def _get_ref_hook(self) -> str | None: + """Generate the ref hook for the component. + + Returns: + The useRef hook for managing refs. + """ + ref = self.get_ref() + if ref is not None: + return f"const {ref} = useRef(null); refs['{ref}'] = {ref};" + + def _get_hooks_internal(self) -> Set[str]: + """Get the React hooks for this component managed by the framework. + + Downstream components should NOT override this method to avoid breaking + framework functionality. + + Returns: + Set of internally managed hooks. + """ + return set( + hook + for hook in [self._get_mount_lifecycle_hook(), self._get_ref_hook()] + if hook + ) + + def _get_hooks(self) -> Optional[str]: + """Get the React hooks for this component. + + Downstream components should override this method to add their own hooks. + + Returns: + The hooks for just this component. + """ + return def get_hooks(self) -> Set[str]: """Get the React hooks for this component and its children. @@ -546,7 +572,7 @@ def get_hooks(self) -> Set[str]: The code that should appear just before returning the rendered component. """ # Store the code in a set to avoid duplicates. - code = set() + code = self._get_hooks_internal() # Add the hook code for this component. hooks = self._get_hooks() From 551a43ff227b3db03bbd9492f12d4c3c9114d6f2 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 18 Aug 2023 09:37:08 -0700 Subject: [PATCH 04/11] test_component: check for new triggers on_mount and on_unmount --- tests/components/test_component.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/components/test_component.py b/tests/components/test_component.py index 23274b80dc..8d6abf7dc8 100644 --- a/tests/components/test_component.py +++ b/tests/components/test_component.py @@ -5,6 +5,7 @@ import reflex as rx from reflex.components.component import Component, CustomComponent, custom_component from reflex.components.layout.box import Box +from reflex.constants import ON_MOUNT, ON_UNMOUNT from reflex.event import EVENT_ARG, EVENT_TRIGGERS, EventHandler from reflex.state import State from reflex.style import Style @@ -343,8 +344,9 @@ def test_get_triggers(component1, component2): component1: A test component. component2: A test component. """ - assert component1().get_triggers() == EVENT_TRIGGERS - assert component2().get_triggers() == {"on_open", "on_close"} | EVENT_TRIGGERS + default_triggers = {ON_MOUNT, ON_UNMOUNT} | EVENT_TRIGGERS + assert component1().get_triggers() == default_triggers + assert component2().get_triggers() == {"on_open", "on_close"} | default_triggers def test_create_custom_component(my_component): From 7c5d6d72f51cfb828d1555000411e963cf0f4b97 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 18 Aug 2023 09:37:50 -0700 Subject: [PATCH 05/11] format: fixup Var import --- reflex/utils/format.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/reflex/utils/format.py b/reflex/utils/format.py index 759dc092db..e0fa2879a7 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -14,8 +14,9 @@ import plotly.graph_objects as go from plotly.io import to_json -from reflex import constants, vars +from reflex import constants from reflex.utils import types +from reflex.vars import Var if TYPE_CHECKING: from reflex.components.component import ComponentStyle @@ -182,7 +183,7 @@ def format_string(string: str) -> str: return string -def format_var(var: vars.Var) -> str: +def format_var(var: Var) -> str: """Format the given Var as a javascript value. Args: @@ -330,8 +331,8 @@ def format_event(event_spec: EventSpec) -> str: def format_event_chain( - event_chain: [EventChain | vars.Var[EventChain]], - event_arg: vars.Var | None = None, + event_chain: [EventChain | Var[EventChain]], + event_arg: Var | None = None, ) -> str: """Format an event chain as a javascript invocation. @@ -345,7 +346,9 @@ def format_event_chain( Raises: ValueError: When the given event chain is not a valid event chain. """ - if isinstance(event_chain, vars.Var): + if isinstance(event_chain, Var): + from reflex.event import EventChain + if event_chain.type_ is not EventChain: raise ValueError(f"Invalid event chain: {event_chain}") return "".join( From 9fb29fa0482e51d2003a10e8e318ddd668ef1b8a Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Sun, 20 Aug 2023 15:04:28 -0700 Subject: [PATCH 06/11] typing fixups --- reflex/components/component.py | 8 ++++---- reflex/utils/format.py | 18 ++++++++++-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/reflex/components/component.py b/reflex/components/component.py index 30ebf404ef..95be262590 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -515,8 +515,8 @@ def _get_mount_lifecycle_hook(self) -> str | None: """ # pop on_mount and on_unmount from event_triggers since these are handled by # hooks, not as actually props in the component - on_mount = self.event_triggers.pop(constants.ON_MOUNT, "") - on_unmount = self.event_triggers.pop(constants.ON_UNMOUNT, "") + on_mount = self.event_triggers.pop(constants.ON_MOUNT, None) + on_unmount = self.event_triggers.pop(constants.ON_UNMOUNT, None) if on_mount: on_mount = format.format_event_chain(on_mount) if on_unmount: @@ -524,9 +524,9 @@ def _get_mount_lifecycle_hook(self) -> str | None: if on_mount or on_unmount: return f""" useEffect(() => {{ - {on_mount} + {on_mount or ""} return () => {{ - {on_unmount} + {on_unmount or ""} }} }}, []);""" diff --git a/reflex/utils/format.py b/reflex/utils/format.py index e0fa2879a7..26a41b2a07 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -20,7 +20,7 @@ if TYPE_CHECKING: from reflex.components.component import ComponentStyle - from reflex.event import EventHandler, EventSpec + from reflex.event import EventChain, EventHandler, EventSpec WRAP_MAP = { "{": "}", @@ -196,9 +196,9 @@ def format_var(var: Var) -> str: return str(var) if types._issubclass(var.type_, str): return format_string(var.full_name) - if format.is_wrapped(var.full_name, "{"): + if is_wrapped(var.full_name, "{"): return var.full_name - return format.json_dumps(var.full_name) + return json_dumps(var.full_name) def format_route(route: str) -> str: @@ -331,7 +331,7 @@ def format_event(event_spec: EventSpec) -> str: def format_event_chain( - event_chain: [EventChain | Var[EventChain]], + event_chain: EventChain | Var[EventChain], event_arg: Var | None = None, ) -> str: """Format an event chain as a javascript invocation. @@ -352,10 +352,12 @@ def format_event_chain( if event_chain.type_ is not EventChain: raise ValueError(f"Invalid event chain: {event_chain}") return "".join( - "(() => {", - format_var(event_chain), - f"; preventDefault({format_var(event_arg)})" if event_arg else "", - "})()", + [ + "(() => {", + format_var(event_chain), + f"; preventDefault({format_var(event_arg)})" if event_arg else "", + "})()", + ] ) chain = ",".join([format_event(event) for event in event_chain.events]) From b77fdf0fe71c5ac659b9f1940edce1e28d1b37db Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Sun, 20 Aug 2023 15:19:24 -0700 Subject: [PATCH 07/11] integration/test_event_chain: add on_mount / on_unmount cases These end up being a little weird due to react StrictMode when running the dev server... so the events are kind of duplicated, since the useEffect gets called twice essentially. --- integration/test_event_chain.py | 88 +++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/integration/test_event_chain.py b/integration/test_event_chain.py index 89a7b64f92..ba0d94103b 100644 --- a/integration/test_event_chain.py +++ b/integration/test_event_chain.py @@ -155,8 +155,32 @@ def on_load_yield_chain(): rx.input(value=State.token, readonly=True, id="token"), ) + def on_mount_return_chain(): + return rx.fragment( + rx.text( + "return", + on_mount=State.on_load_return_chain, + on_unmount=lambda: State.event_arg("unmount"), # type: ignore + ), + rx.input(value=State.token, readonly=True, id="token"), + rx.button("Unmount", on_click=rx.redirect("/"), id="unmount"), + ) + + def on_mount_yield_chain(): + return rx.fragment( + rx.text( + "yield", + on_mount=State.on_load_yield_chain, + on_unmount=State.event_no_args, + ), + rx.input(value=State.token, readonly=True, id="token"), + rx.button("Unmount", on_click=rx.redirect("/"), id="unmount"), + ) + app.add_page(on_load_return_chain, on_load=State.on_load_return_chain) # type: ignore app.add_page(on_load_yield_chain, on_load=State.on_load_yield_chain) # type: ignore + app.add_page(on_mount_return_chain) + app.add_page(on_mount_yield_chain) app.compile() @@ -330,3 +354,67 @@ def test_event_chain_on_load(event_chain, driver, uri, exp_event_order): time.sleep(0.5) backend_state = event_chain.app_instance.state_manager.states[token] assert backend_state.event_order == exp_event_order + + +@pytest.mark.parametrize( + ("uri", "exp_event_order"), + [ + ( + "/on-mount-return-chain", + [ + "on_load_return_chain", + "event_arg:unmount", + "on_load_return_chain", + "event_arg:1", + "event_arg:2", + "event_arg:3", + "event_arg:1", + "event_arg:2", + "event_arg:3", + "event_arg:unmount", + ], + ), + ( + "/on-mount-yield-chain", + [ + "on_load_yield_chain", + "event_no_args", + "on_load_yield_chain", + "event_arg:4", + "event_arg:5", + "event_arg:6", + "event_arg:4", + "event_arg:5", + "event_arg:6", + "event_no_args", + ], + ), + ], +) +def test_event_chain_on_mount(event_chain, driver, uri, exp_event_order): + """Load the URI, assert that the events are handled in the correct order. + + These pages use `on_mount` and `on_unmount`, which get fired twice in dev mode + due to react StrictMode being used. + + In prod mode, these events are only fired once. + + Args: + event_chain: AppHarness for the event_chain app + driver: selenium WebDriver open to the app + uri: the page to load + exp_event_order: the expected events recorded in the State + """ + driver.get(event_chain.frontend_url + uri) + token_input = driver.find_element(By.ID, "token") + assert token_input + + token = event_chain.poll_for_value(token_input) + + unmount_button = driver.find_element(By.ID, "unmount") + assert unmount_button + unmount_button.click() + + time.sleep(1) + backend_state = event_chain.app_instance.state_manager.states[token] + assert backend_state.event_order == exp_event_order From 682c247b8c9ad70194f308bc18e21e8edcef4339 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Sun, 20 Aug 2023 15:24:57 -0700 Subject: [PATCH 08/11] test_event_chain: pass list to `on_mount` --- integration/test_event_chain.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/integration/test_event_chain.py b/integration/test_event_chain.py index ba0d94103b..f762f16cae 100644 --- a/integration/test_event_chain.py +++ b/integration/test_event_chain.py @@ -170,7 +170,10 @@ def on_mount_yield_chain(): return rx.fragment( rx.text( "yield", - on_mount=State.on_load_yield_chain, + on_mount=[ + State.on_load_yield_chain, + lambda: State.event_arg("mount"), # type: ignore + ], on_unmount=State.event_no_args, ), rx.input(value=State.token, readonly=True, id="token"), @@ -378,8 +381,10 @@ def test_event_chain_on_load(event_chain, driver, uri, exp_event_order): "/on-mount-yield-chain", [ "on_load_yield_chain", + "event_arg:mount", "event_no_args", "on_load_yield_chain", + "event_arg:mount", "event_arg:4", "event_arg:5", "event_arg:6", From 5959362b130ac412a47c328d52fc69cdf4c5023b Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 25 Aug 2023 11:39:45 -0700 Subject: [PATCH 09/11] XXX: try out CI against reflex-web tip --- .github/workflows/integration_tests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 76b4134739..7736115cf5 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -106,7 +106,8 @@ jobs: uses: actions/checkout@v3 with: repository: reflex-dev/reflex-web - ref: reflex-ci + ref: 4503225add545b9f38f3268648b86c37ecf00507 + #ref: reflex-ci path: reflex-web - name: Install Requirements for reflex-web From eb49f04d16fdf111cfa9977e12bfe5627bcff4cd Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Mon, 28 Aug 2023 21:20:06 -0700 Subject: [PATCH 10/11] Revert "XXX: try out CI against reflex-web tip" This reverts commit 5959362b130ac412a47c328d52fc69cdf4c5023b. --- .github/workflows/integration_tests.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 7736115cf5..76b4134739 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -106,8 +106,7 @@ jobs: uses: actions/checkout@v3 with: repository: reflex-dev/reflex-web - ref: 4503225add545b9f38f3268648b86c37ecf00507 - #ref: reflex-ci + ref: reflex-ci path: reflex-web - name: Install Requirements for reflex-web From f416413d9e3bbbb9cb18b78daf781e3620133fc1 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Tue, 29 Aug 2023 12:20:43 -0700 Subject: [PATCH 11/11] Update pininput and rangeslider to use _get_ref_hooks Now the ref hooks are managed by a separate internal function so that downstream component wrappers can worry less about breaking framework functionality when overriding `_get_hooks`; but this means any internal functionality that was previously overriding `_get_hooks` was still unconditionally getting the base `_get_ref_hooks` functionality, that is was intentionally trying to override. Really glad we wrote integration tests for this behavior! --- reflex/components/forms/pininput.py | 8 ++++---- reflex/components/forms/rangeslider.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/reflex/components/forms/pininput.py b/reflex/components/forms/pininput.py index 81ec13996a..0e889cc234 100644 --- a/reflex/components/forms/pininput.py +++ b/reflex/components/forms/pininput.py @@ -76,8 +76,8 @@ def get_ref(self): """ return None - def _get_hooks(self) -> Optional[str]: - """Override the base get_hooks to handle array refs. + def _get_ref_hook(self) -> Optional[str]: + """Override the base _get_ref_hook to handle array refs. Returns: The overrided hooks. @@ -86,7 +86,7 @@ def _get_hooks(self) -> Optional[str]: ref = format.format_array_ref(self.id, None) if ref: return f"const {ref} = Array.from({{length:{self.length}}}, () => useRef(null));" - return super()._get_hooks() + return super()._get_ref_hook() @classmethod def create(cls, *children, **props) -> Component: @@ -130,7 +130,7 @@ class PinInputField(ChakraComponent): # Default to None because it is assigned by PinInput when created. index: Optional[Var[int]] = None - def _get_hooks(self) -> Optional[str]: + def _get_ref_hook(self) -> Optional[str]: return None def get_ref(self): diff --git a/reflex/components/forms/rangeslider.py b/reflex/components/forms/rangeslider.py index b27e33205e..c72c0f0efe 100644 --- a/reflex/components/forms/rangeslider.py +++ b/reflex/components/forms/rangeslider.py @@ -64,8 +64,8 @@ def get_ref(self): """ return None - def _get_hooks(self) -> Optional[str]: - """Override the base get_hooks to handle array refs. + def _get_ref_hook(self) -> Optional[str]: + """Override the base _get_ref_hook to handle array refs. Returns: The overrided hooks. @@ -74,7 +74,7 @@ def _get_hooks(self) -> Optional[str]: ref = format.format_array_ref(self.id, None) if ref: return f"const {ref} = Array.from({{length:2}}, () => useRef(null));" - return super()._get_hooks() + return super()._get_ref_hook() @classmethod def create(cls, *children, **props) -> Component: @@ -130,7 +130,7 @@ class RangeSliderThumb(ChakraComponent): # The position of the thumb. index: Var[int] - def _get_hooks(self) -> Optional[str]: + def _get_ref_hook(self) -> Optional[str]: # hook is None because RangeSlider is handling it. return None