11from __future__ import annotations
22
33import logging
4+ import sys
45from asyncio import Event , Task , create_task , gather
6+ from contextvars import ContextVar , Token
57from typing import Any , Callable , Protocol , TypeVar
68
79from anyio import Semaphore
810
911from reactpy .core ._thread_local import ThreadLocal
1012from reactpy .types import ComponentType , Context , ContextProviderType
13+ from reactpy .utils import Singleton
1114
1215T = TypeVar ("T" )
1316
@@ -18,16 +21,39 @@ async def __call__(self, stop: Event) -> None: ...
1821
1922logger = logging .getLogger (__name__ )
2023
21- _HOOK_STATE : ThreadLocal [list [LifeCycleHook ]] = ThreadLocal (list )
2224
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."""
2329
24- def current_hook () -> LifeCycleHook :
25- """Get the current :class:`LifeCycleHook`"""
26- hook_stack = _HOOK_STATE .get ()
27- if not hook_stack :
28- msg = "No life cycle hook is active. Are you rendering in a layout?"
29- raise RuntimeError (msg )
30- return hook_stack [- 1 ]
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 ]
54+
55+
56+ HOOK_STACK = _HookStack ()
3157
3258
3359class LifeCycleHook :
@@ -37,7 +63,7 @@ class LifeCycleHook:
3763 a component is first rendered until it is removed from the layout. The life cycle
3864 is ultimately driven by the layout itself, but components can "hook" into those
3965 events to perform actions. Components gain access to their own life cycle hook
40- 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:
4167
4268 1. Adding state via :meth:`use_state`
4369 2. Adding effects via :meth:`add_effect`
@@ -57,7 +83,7 @@ class LifeCycleHook:
5783 .. testcode::
5884
5985 from reactpy.core._life_cycle_hook import LifeCycleHook
60- from reactpy.core.hooks import current_hook
86+ from reactpy.core.hooks import HOOK_STACK
6187
6288 # this function will come from a layout implementation
6389 schedule_render = lambda: ...
@@ -75,15 +101,15 @@ class LifeCycleHook:
75101 ...
76102
77103 # the component may access the current hook
78- assert current_hook() is hook
104+ assert HOOK_STACK. current_hook() is hook
79105
80106 # and save state or add effects
81- current_hook().use_state(lambda: ...)
107+ HOOK_STACK. current_hook().use_state(lambda: ...)
82108
83109 async def my_effect(stop_event):
84110 ...
85111
86- current_hook().add_effect(my_effect)
112+ HOOK_STACK. current_hook().add_effect(my_effect)
87113 finally:
88114 await hook.affect_component_did_render()
89115
@@ -130,7 +156,7 @@ def __init__(
130156 self ._scheduled_render = False
131157 self ._rendered_atleast_once = False
132158 self ._current_state_index = 0
133- self ._state : tuple [ Any , ...] = ()
159+ self ._state : list = []
134160 self ._effect_funcs : list [EffectFunc ] = []
135161 self ._effect_tasks : list [Task [None ]] = []
136162 self ._effect_stops : list [Event ] = []
@@ -157,7 +183,7 @@ def use_state(self, function: Callable[[], T]) -> T:
157183 if not self ._rendered_atleast_once :
158184 # since we're not initialized yet we're just appending state
159185 result = function ()
160- self ._state += (result , )
186+ self ._state . append (result )
161187 else :
162188 # once finalized we iterate over each succesively used piece of state
163189 result = self ._state [self ._current_state_index ]
@@ -232,13 +258,13 @@ def set_current(self) -> None:
232258 This method is called by a layout before entering the render method
233259 of this hook's associated component.
234260 """
235- hook_stack = _HOOK_STATE .get ()
261+ hook_stack = HOOK_STACK .get ()
236262 if hook_stack :
237263 parent = hook_stack [- 1 ]
238264 self ._context_providers .update (parent ._context_providers )
239265 hook_stack .append (self )
240266
241267 def unset_current (self ) -> None :
242268 """Unset this hook as the active hook in this thread"""
243- if _HOOK_STATE .get ().pop () is not self :
269+ if HOOK_STACK .get ().pop () is not self :
244270 raise RuntimeError ("Hook stack is in an invalid state" ) # nocov
0 commit comments