Skip to content

Commit 3136175

Browse files
committed
Create separate use_async_effect hook
1 parent b1e66e2 commit 3136175

File tree

6 files changed

+89
-36
lines changed

6 files changed

+89
-36
lines changed

docs/source/reference/_examples/simple_dashboard.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def RandomWalkGraph(mu, sigma):
4949
interval = use_interval(0.5)
5050
data, set_data = reactpy.hooks.use_state([{"x": 0, "y": 0}] * 50)
5151

52-
@reactpy.hooks.use_effect
52+
@reactpy.hooks.use_async_effect
5353
async def animate():
5454
await interval
5555
last_data_point = data[-1]

docs/source/reference/_examples/snake_game.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def on_direction_change(event):
9090

9191
interval = use_interval(0.5)
9292

93-
@reactpy.hooks.use_effect
93+
@reactpy.hooks.use_async_effect
9494
async def animate():
9595
if new_game_state is not None:
9696
await asyncio.sleep(1)

src/reactpy/core/hooks.py

+77-27
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@
3030

3131

3232
__all__ = [
33-
"use_state",
33+
"use_callback",
3434
"use_effect",
35+
"use_memo",
3536
"use_reducer",
36-
"use_callback",
3737
"use_ref",
38-
"use_memo",
38+
"use_state",
3939
]
4040

4141
logger = getLogger(__name__)
@@ -110,15 +110,15 @@ def use_effect(
110110

111111
@overload
112112
def use_effect(
113-
function: _EffectApplyFunc,
113+
function: _SyncEffectFunc,
114114
dependencies: Sequence[Any] | ellipsis | None = ...,
115115
) -> None: ...
116116

117117

118118
def use_effect(
119-
function: _EffectApplyFunc | None = None,
119+
function: _SyncEffectFunc | None = None,
120120
dependencies: Sequence[Any] | ellipsis | None = ...,
121-
) -> Callable[[_EffectApplyFunc], None] | None:
121+
) -> Callable[[_SyncEffectFunc], None] | None:
122122
"""See the full :ref:`Use Effect` docs for details
123123
124124
Parameters:
@@ -134,37 +134,87 @@ def use_effect(
134134
If not function is provided, a decorator. Otherwise ``None``.
135135
"""
136136
hook = current_hook()
137-
138137
dependencies = _try_to_infer_closure_values(function, dependencies)
139138
memoize = use_memo(dependencies=dependencies)
140139
last_clean_callback: Ref[_EffectCleanFunc | None] = use_ref(None)
141140

142-
def add_effect(function: _EffectApplyFunc) -> None:
143-
if not asyncio.iscoroutinefunction(function):
144-
sync_function = cast(_SyncEffectFunc, function)
145-
else:
146-
async_function = cast(_AsyncEffectFunc, function)
141+
def add_effect(function: _SyncEffectFunc) -> None:
142+
async def effect(stop: asyncio.Event) -> None:
143+
if last_clean_callback.current is not None:
144+
last_clean_callback.current()
145+
last_clean_callback.current = None
146+
clean = last_clean_callback.current = function()
147+
await stop.wait()
148+
if clean is not None:
149+
clean()
150+
151+
return memoize(lambda: hook.add_effect(effect))
152+
153+
if function is not None:
154+
add_effect(function)
155+
return None
156+
157+
return add_effect
158+
159+
160+
@overload
161+
def use_async_effect(
162+
function: None = None,
163+
dependencies: Sequence[Any] | ellipsis | None = ...,
164+
) -> Callable[[_EffectApplyFunc], None]: ...
147165

148-
def sync_function() -> _EffectCleanFunc | None:
149-
task = asyncio.create_task(async_function())
150166

151-
def clean_future() -> None:
152-
if not task.cancel():
153-
try:
154-
clean = task.result()
155-
except asyncio.CancelledError:
156-
pass
157-
else:
158-
if clean is not None:
159-
clean()
167+
@overload
168+
def use_async_effect(
169+
function: _AsyncEffectFunc,
170+
dependencies: Sequence[Any] | ellipsis | None = ...,
171+
) -> None: ...
172+
173+
174+
def use_async_effect(
175+
function: _AsyncEffectFunc | None = None,
176+
dependencies: Sequence[Any] | ellipsis | None = ...,
177+
) -> Callable[[_AsyncEffectFunc], None] | None:
178+
"""See the full :ref:`Use Effect` docs for details
179+
180+
Parameters:
181+
function:
182+
Applies the effect and can return a clean-up function
183+
dependencies:
184+
Dependencies for the effect. The effect will only trigger if the identity
185+
of any value in the given sequence changes (i.e. their :func:`id` is
186+
different). By default these are inferred based on local variables that are
187+
referenced by the given function.
188+
189+
Returns:
190+
If not function is provided, a decorator. Otherwise ``None``.
191+
"""
192+
hook = current_hook()
193+
dependencies = _try_to_infer_closure_values(function, dependencies)
194+
memoize = use_memo(dependencies=dependencies)
195+
last_clean_callback: Ref[_EffectCleanFunc | None] = use_ref(None)
196+
197+
def add_effect(function: _AsyncEffectFunc) -> None:
198+
def sync_executor() -> _EffectCleanFunc | None:
199+
task = asyncio.create_task(function())
160200

161-
return clean_future
201+
def clean_future() -> None:
202+
if not task.cancel():
203+
try:
204+
clean = task.result()
205+
except asyncio.CancelledError:
206+
pass
207+
else:
208+
if clean is not None:
209+
clean()
210+
211+
return clean_future
162212

163213
async def effect(stop: asyncio.Event) -> None:
164214
if last_clean_callback.current is not None:
165215
last_clean_callback.current()
166216
last_clean_callback.current = None
167-
clean = last_clean_callback.current = sync_function()
217+
clean = last_clean_callback.current = sync_executor()
168218
await stop.wait()
169219
if clean is not None:
170220
clean()
@@ -174,8 +224,8 @@ async def effect(stop: asyncio.Event) -> None:
174224
if function is not None:
175225
add_effect(function)
176226
return None
177-
else:
178-
return add_effect
227+
228+
return add_effect
179229

180230

181231
def use_debug_value(

tests/test_core/test_hooks.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ async def test_use_async_effect():
481481

482482
@reactpy.component
483483
def ComponentWithAsyncEffect():
484-
@reactpy.hooks.use_effect
484+
@reactpy.hooks.use_async_effect
485485
async def effect():
486486
effect_ran.set()
487487

@@ -500,7 +500,9 @@ async def test_use_async_effect_cleanup():
500500
@reactpy.component
501501
@component_hook.capture
502502
def ComponentWithAsyncEffect():
503-
@reactpy.hooks.use_effect(dependencies=None) # force this to run every time
503+
@reactpy.hooks.use_async_effect(
504+
dependencies=None
505+
) # force this to run every time
504506
async def effect():
505507
effect_ran.set()
506508
return cleanup_ran.set
@@ -527,7 +529,8 @@ async def test_use_async_effect_cancel(caplog):
527529
@reactpy.component
528530
@component_hook.capture
529531
def ComponentWithLongWaitingEffect():
530-
@reactpy.hooks.use_effect(dependencies=None) # force this to run every time
532+
# force this to run every time
533+
@reactpy.hooks.use_async_effect(dependencies=None)
531534
async def effect():
532535
effect_ran.set()
533536
try:

tests/test_core/test_layout.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from reactpy import html
1313
from reactpy.config import REACTPY_ASYNC_RENDERING, REACTPY_DEBUG
1414
from reactpy.core.component import component
15-
from reactpy.core.hooks import use_effect, use_state
15+
from reactpy.core.hooks import use_async_effect, use_effect, use_state
1616
from reactpy.core.layout import Layout
1717
from reactpy.testing import (
1818
HookCatcher,
@@ -1016,7 +1016,7 @@ def Parent():
10161016
def Child(child_key):
10171017
state, set_state = use_state(0)
10181018

1019-
@use_effect
1019+
@use_async_effect
10201020
async def record_if_state_is_reset():
10211021
if state:
10221022
return

tests/test_html.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def on_click(event):
1717
set_count(count + 1)
1818

1919
return html.div(
20-
html.div({"id": "mount-count", "dataValue": 0}),
20+
html.div({"id": "mount-count", "data-value": 0}),
2121
html.script(
2222
f'document.getElementById("mount-count").setAttribute("data-value", {count});'
2323
),
@@ -57,7 +57,7 @@ def HasScript():
5757
return html.div()
5858
else:
5959
return html.div(
60-
html.div({"id": "run-count", "dataValue": 0}),
60+
html.div({"id": "run-count", "data-value": 0}),
6161
html.script(
6262
{
6363
"src": f"/reactpy/modules/{file_name_template.format(src_id=src_id)}"

0 commit comments

Comments
 (0)