Skip to content

Commit a4fc2f5

Browse files
committed
simpler add_effect interface
1 parent 387dc05 commit a4fc2f5

File tree

2 files changed

+41
-39
lines changed

2 files changed

+41
-39
lines changed

src/py/reactpy/reactpy/core/_life_cycle_hook.py

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

33
import logging
44
from asyncio import gather
5-
from collections.abc import AsyncGenerator
5+
from collections.abc import Awaitable
66
from typing import Any, Callable, TypeVar
77

88
from anyio import Semaphore
@@ -41,7 +41,7 @@ class LifeCycleHook:
4141
.. testcode::
4242
4343
from reactpy.core._life_cycle_hook import LifeCycleHook
44-
from reactpy.core.hooks import current_hook, COMPONENT_DID_RENDER_EFFECT
44+
from reactpy.core.hooks import current_hook
4545
4646
# this function will come from a layout implementation
4747
schedule_render = lambda: ...
@@ -63,7 +63,11 @@ class LifeCycleHook:
6363
6464
# and save state or add effects
6565
current_hook().use_state(lambda: ...)
66-
current_hook().add_effect(COMPONENT_DID_RENDER_EFFECT, lambda: ...)
66+
67+
async def effect():
68+
yield
69+
70+
current_hook().add_effect(effect)
6771
finally:
6872
await hook.affect_component_did_render()
6973
@@ -88,10 +92,10 @@ class LifeCycleHook:
8892
"__weakref__",
8993
"_context_providers",
9094
"_current_state_index",
91-
"_pending_effects",
95+
"_effect_cleanups",
96+
"_effect_startups",
9297
"_render_access",
9398
"_rendered_atleast_once",
94-
"_running_effects",
9599
"_schedule_render_callback",
96100
"_schedule_render_later",
97101
"_state",
@@ -110,8 +114,8 @@ def __init__(
110114
self._rendered_atleast_once = False
111115
self._current_state_index = 0
112116
self._state: tuple[Any, ...] = ()
113-
self._pending_effects: list[AsyncGenerator[None, None]] = []
114-
self._running_effects: list[AsyncGenerator[None, None]] = []
117+
self._effect_startups: list[Callable[[], Awaitable[None]]] = []
118+
self._effect_cleanups: list[Callable[[], Awaitable[None]]] = []
115119
self._render_access = Semaphore(1) # ensure only one render at a time
116120

117121
def schedule_render(self) -> None:
@@ -131,9 +135,14 @@ def use_state(self, function: Callable[[], T]) -> T:
131135
self._current_state_index += 1
132136
return result
133137

134-
def add_effect(self, effect_func: Callable[[], AsyncGenerator[None, None]]) -> None:
138+
def add_effect(
139+
self,
140+
start_effect: Callable[[], Awaitable[None]],
141+
clean_effect: Callable[[], Awaitable[None]],
142+
) -> None:
135143
"""Add an effect to this hook"""
136-
self._pending_effects.append(effect_func())
144+
self._effect_startups.append(start_effect)
145+
self._effect_cleanups.append(clean_effect)
137146

138147
def set_context_provider(self, provider: ContextProviderType[Any]) -> None:
139148
self._context_providers[provider.type] = provider
@@ -155,29 +164,28 @@ async def affect_component_did_render(self) -> None:
155164
self._rendered_atleast_once = True
156165
self._current_state_index = 0
157166
self._render_access.release()
167+
del self.component
158168

159169
async def affect_layout_did_render(self) -> None:
160170
"""The layout completed a render"""
161171
try:
162-
await gather(*[g.asend(None) for g in self._pending_effects])
163-
self._running_effects.extend(self._pending_effects)
172+
await gather(*[start() for start in self._effect_startups])
164173
except Exception:
165174
logger.exception("Error during effect startup")
166175
finally:
167-
self._pending_effects.clear()
168-
if self._schedule_render_later:
169-
self._schedule_render()
170-
self._schedule_render_later = False
171-
del self.component
176+
self._effect_startups.clear()
177+
if self._schedule_render_later:
178+
self._schedule_render()
179+
self._schedule_render_later = False
172180

173181
async def affect_component_will_unmount(self) -> None:
174182
"""The component is about to be removed from the layout"""
175183
try:
176-
await gather(*[g.aclose() for g in self._running_effects])
184+
await gather(*[clean() for clean in self._effect_cleanups])
177185
except Exception:
178186
logger.exception("Error during effect cleanup")
179187
finally:
180-
self._running_effects.clear()
188+
self._effect_cleanups.clear()
181189

182190
def set_current(self) -> None:
183191
"""Set this hook as the active hook in this thread

src/py/reactpy/reactpy/core/hooks.py

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
import asyncio
4-
from collections.abc import AsyncGenerator, Awaitable, Sequence
4+
from collections.abc import Coroutine, Sequence
55
from logging import getLogger
66
from types import FunctionType
77
from typing import (
@@ -95,7 +95,9 @@ def dispatch(new: _Type | Callable[[_Type], _Type]) -> None:
9595

9696
_EffectCleanFunc: TypeAlias = "Callable[[], None]"
9797
_SyncEffectFunc: TypeAlias = "Callable[[], _EffectCleanFunc | None]"
98-
_AsyncEffectFunc: TypeAlias = "Callable[[], Awaitable[_EffectCleanFunc | None]]"
98+
_AsyncEffectFunc: TypeAlias = (
99+
"Callable[[], Coroutine[None, None, _EffectCleanFunc | None]]"
100+
)
99101
_EffectApplyFunc: TypeAlias = "_SyncEffectFunc | _AsyncEffectFunc"
100102

101103

@@ -146,36 +148,28 @@ def add_effect(function: _EffectApplyFunc) -> None:
146148
async_function = cast(_AsyncEffectFunc, function)
147149

148150
def sync_function() -> _EffectCleanFunc | None:
149-
future = asyncio.ensure_future(async_function())
151+
task = asyncio.create_task(async_function())
150152

151153
def clean_future() -> None:
152-
if not future.cancel():
153-
clean = future.result()
154+
if not task.cancel():
155+
clean = task.result()
154156
if clean is not None:
155157
clean()
156158

157159
return clean_future
158160

159-
async def effect() -> AsyncGenerator[None, None]:
161+
async def start_effect() -> None:
160162
if last_clean_callback.current is not None:
161163
last_clean_callback.current()
164+
last_clean_callback.current = None
165+
last_clean_callback.current = sync_function()
162166

163-
cleaned = False
164-
clean = sync_function()
165-
166-
def callback() -> None:
167-
nonlocal cleaned
168-
if clean and not cleaned:
169-
cleaned = True
170-
clean()
171-
172-
last_clean_callback.current = callback
173-
try:
174-
yield
175-
finally:
176-
callback()
167+
async def clean_effect() -> None:
168+
if last_clean_callback.current is not None:
169+
last_clean_callback.current()
170+
last_clean_callback.current = None
177171

178-
return memoize(lambda: hook.add_effect(effect))
172+
return memoize(lambda: hook.add_effect(start_effect, clean_effect))
179173

180174
if function is not None:
181175
add_effect(function)

0 commit comments

Comments
 (0)