11from __future__ import annotations
22
33import logging
4+ import sys
45from asyncio import Event , Task , create_task , gather
56from contextvars import ContextVar , Token
67from typing import Any , Callable , Protocol , TypeVar
78
89from anyio import Semaphore
910
11+ from reactpy .core ._thread_local import ThreadLocal
1012from reactpy .types import ComponentType , Context , ContextProviderType
13+ from reactpy .utils import Singleton
1114
1215T = TypeVar ("T" )
1316
@@ -17,23 +20,40 @@ async def __call__(self, stop: Event) -> None: ...
1720
1821
1922logger = logging .getLogger (__name__ )
20- _HOOK_STATE : ContextVar [list [LifeCycleHook ]] = ContextVar ("_hook_state" )
2123
2224
23- def clear_hook_state (token : Token [list ]) -> None :
24- hook_stack = _HOOK_STATE .get ()
25- if hook_stack :
26- logger .warning ("clear_hook_state: Hook stack was not empty" )
27- _HOOK_STATE .reset (token )
25+ class __HookStack (Singleton ): # pragma: no cover
26+ """A singleton object which manages the current component tree's hooks.
27+ Life cycle hooks can be stored in a thread local or context variable depending
28+ on the platform."""
29+
30+ _state : ThreadLocal [list [LifeCycleHook ]] | ContextVar [list [LifeCycleHook ]] = (
31+ ThreadLocal (list ) if sys .platform == "emscripten" else ContextVar ("hook_state" )
32+ )
33+
34+ def get (self ) -> list [LifeCycleHook ]:
35+ return self ._state .get ()
36+
37+ def initialize (self ) -> Token [list [LifeCycleHook ]] | None :
38+ return None if isinstance (self ._state , ThreadLocal ) else self ._state .set ([])
39+
40+ def reset (self , token : Token [list [LifeCycleHook ]] | None ) -> None :
41+ if isinstance (self ._state , ThreadLocal ):
42+ self ._state .get ().clear ()
43+ elif token :
44+ self ._state .reset (token )
45+ else :
46+ raise RuntimeError ("Hook stack is an ContextVar but no token was provided" )
47+
48+ def current_hook (self ) -> LifeCycleHook :
49+ hook_stack = self .get ()
50+ if not hook_stack :
51+ msg = "No life cycle hook is active. Are you rendering in a layout?"
52+ raise RuntimeError (msg )
53+ return hook_stack [- 1 ]
2854
2955
30- def current_hook () -> LifeCycleHook :
31- """Get the current :class:`LifeCycleHook`"""
32- hook_stack = _HOOK_STATE .get ()
33- if not hook_stack :
34- msg = "No life cycle hook is active. Are you rendering in a layout?"
35- raise RuntimeError (msg )
36- return hook_stack [- 1 ]
56+ HOOK_STACK = __HookStack ()
3757
3858
3959class LifeCycleHook :
@@ -43,7 +63,7 @@ class LifeCycleHook:
4363 a component is first rendered until it is removed from the layout. The life cycle
4464 is ultimately driven by the layout itself, but components can "hook" into those
4565 events to perform actions. Components gain access to their own life cycle hook
46- by calling :func:`current_hook`. They can then perform actions such as:
66+ by calling :func:`HOOK_STACK. current_hook`. They can then perform actions such as:
4767
4868 1. Adding state via :meth:`use_state`
4969 2. Adding effects via :meth:`add_effect`
@@ -63,7 +83,7 @@ class LifeCycleHook:
6383 .. testcode::
6484
6585 from reactpy.core._life_cycle_hook import LifeCycleHook
66- from reactpy.core.hooks import current_hook
86+ from reactpy.core.hooks import HOOK_STACK
6787
6888 # this function will come from a layout implementation
6989 schedule_render = lambda: ...
@@ -81,15 +101,15 @@ class LifeCycleHook:
81101 ...
82102
83103 # the component may access the current hook
84- assert current_hook() is hook
104+ assert HOOK_STACK. current_hook() is hook
85105
86106 # and save state or add effects
87- current_hook().use_state(lambda: ...)
107+ HOOK_STACK. current_hook().use_state(lambda: ...)
88108
89109 async def my_effect(stop_event):
90110 ...
91111
92- current_hook().add_effect(my_effect)
112+ HOOK_STACK. current_hook().add_effect(my_effect)
93113 finally:
94114 await hook.affect_component_did_render()
95115
@@ -238,13 +258,13 @@ def set_current(self) -> None:
238258 This method is called by a layout before entering the render method
239259 of this hook's associated component.
240260 """
241- hook_stack = _HOOK_STATE .get ()
261+ hook_stack = HOOK_STACK .get ()
242262 if hook_stack :
243263 parent = hook_stack [- 1 ]
244264 self ._context_providers .update (parent ._context_providers )
245265 hook_stack .append (self )
246266
247267 def unset_current (self ) -> None :
248268 """Unset this hook as the active hook in this thread"""
249- if _HOOK_STATE .get ().pop () is not self :
269+ if HOOK_STACK .get ().pop () is not self :
250270 raise RuntimeError ("Hook stack is in an invalid state" ) # nocov
0 commit comments