Skip to content

Commit 8664a64

Browse files
Bug fix for corrupted hook state - change hook state to a contextvar
--------- Co-authored-by: James Hutchison <[email protected]>
1 parent 17f2286 commit 8664a64

File tree

2 files changed

+38
-18
lines changed

2 files changed

+38
-18
lines changed

src/reactpy/core/_life_cycle_hook.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
import logging
44
from asyncio import Event, Task, create_task, gather
5+
from contextvars import ContextVar, Token
56
from typing import Any, Callable, Protocol, TypeVar
67

78
from anyio import Semaphore
89

9-
from reactpy.core._thread_local import ThreadLocal
1010
from reactpy.core.types import ComponentType, Context, ContextProviderType
1111

1212
T = TypeVar("T")
@@ -18,12 +18,27 @@ async def __call__(self, stop: Event) -> None: ...
1818

1919
logger = logging.getLogger(__name__)
2020

21-
_HOOK_STATE: ThreadLocal[list[LifeCycleHook]] = ThreadLocal(list)
21+
_hook_state = ContextVar("_hook_state")
22+
23+
24+
def create_hook_state(initial: list | None = None) -> Token[list]:
25+
return _hook_state.set(initial or [])
26+
27+
28+
def clear_hook_state(token: Token[list]) -> None:
29+
hook_stack = _hook_state.get()
30+
if hook_stack:
31+
logger.warning("clear_hook_state: Hook stack was not empty")
32+
_hook_state.reset(token)
33+
34+
35+
def get_hook_state() -> list[LifeCycleHook]:
36+
return _hook_state.get()
2237

2338

2439
def current_hook() -> LifeCycleHook:
2540
"""Get the current :class:`LifeCycleHook`"""
26-
hook_stack = _HOOK_STATE.get()
41+
hook_stack = _hook_state.get()
2742
if not hook_stack:
2843
msg = "No life cycle hook is active. Are you rendering in a layout?"
2944
raise RuntimeError(msg)
@@ -130,7 +145,7 @@ def __init__(
130145
self._scheduled_render = False
131146
self._rendered_atleast_once = False
132147
self._current_state_index = 0
133-
self._state: tuple[Any, ...] = ()
148+
self._state: list = []
134149
self._effect_funcs: list[EffectFunc] = []
135150
self._effect_tasks: list[Task[None]] = []
136151
self._effect_stops: list[Event] = []
@@ -157,7 +172,7 @@ def use_state(self, function: Callable[[], T]) -> T:
157172
if not self._rendered_atleast_once:
158173
# since we're not initialized yet we're just appending state
159174
result = function()
160-
self._state += (result,)
175+
self._state.append(result)
161176
else:
162177
# once finalized we iterate over each succesively used piece of state
163178
result = self._state[self._current_state_index]
@@ -232,13 +247,13 @@ def set_current(self) -> None:
232247
This method is called by a layout before entering the render method
233248
of this hook's associated component.
234249
"""
235-
hook_stack = _HOOK_STATE.get()
250+
hook_stack = get_hook_state()
236251
if hook_stack:
237252
parent = hook_stack[-1]
238253
self._context_providers.update(parent._context_providers)
239254
hook_stack.append(self)
240255

241256
def unset_current(self) -> None:
242257
"""Unset this hook as the active hook in this thread"""
243-
if _HOOK_STATE.get().pop() is not self:
258+
if get_hook_state().pop() is not self:
244259
raise RuntimeError("Hook stack is in an invalid state") # nocov

src/reactpy/core/serve.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from anyio.abc import TaskGroup
1010

1111
from reactpy.config import REACTPY_DEBUG_MODE
12+
from reactpy.core._life_cycle_hook import clear_hook_state, create_hook_state
1213
from reactpy.core.types import LayoutEventMessage, LayoutType, LayoutUpdateMessage
1314

1415
logger = getLogger(__name__)
@@ -58,18 +59,22 @@ async def _single_outgoing_loop(
5859
layout: LayoutType[LayoutUpdateMessage, LayoutEventMessage], send: SendCoroutine
5960
) -> None:
6061
while True:
61-
update = await layout.render()
62+
token = create_hook_state()
6263
try:
63-
await send(update)
64-
except Exception: # nocov
65-
if not REACTPY_DEBUG_MODE.current:
66-
msg = (
67-
"Failed to send update. More info may be available "
68-
"if you enabling debug mode by setting "
69-
"`reactpy.config.REACTPY_DEBUG_MODE.current = True`."
70-
)
71-
logger.error(msg)
72-
raise
64+
update = await layout.render()
65+
try:
66+
await send(update)
67+
except Exception: # nocov
68+
if not REACTPY_DEBUG_MODE.current:
69+
msg = (
70+
"Failed to send update. More info may be available "
71+
"if you enabling debug mode by setting "
72+
"`reactpy.config.REACTPY_DEBUG_MODE.current = True`."
73+
)
74+
logger.error(msg)
75+
raise
76+
finally:
77+
clear_hook_state(token)
7378

7479

7580
async def _single_incoming_loop(

0 commit comments

Comments
 (0)