Skip to content

Commit 5945c7a

Browse files
committed
rewrite use_async_effect
1 parent 362e80d commit 5945c7a

File tree

1 file changed

+47
-13
lines changed

1 file changed

+47
-13
lines changed

src/reactpy/core/hooks.py

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -141,17 +141,19 @@ def use_effect(
141141

142142
def decorator(func: _SyncEffectFunc) -> None:
143143
async def effect(stop: asyncio.Event) -> None:
144-
if cleanup_func.current:
145-
cleanup_func.current()
146-
cleanup_func.current = None
144+
# Since the effect is asynchronous, we need to make sure we
145+
# always clean up the previous effect's resources
146+
run_effect_cleanup(cleanup_func)
147147

148148
# Execute the effect and store the clean-up function
149149
cleanup_func.current = func()
150150

151-
# Run the clean-up function when the effect is stopped
151+
# Wait until we get the signal to stop this effect
152152
await stop.wait()
153-
if cleanup_func.current:
154-
cleanup_func.current()
153+
154+
# Run the clean-up function when the effect is stopped,
155+
# if it hasn't been run already by a new effect
156+
run_effect_cleanup(cleanup_func)
155157

156158
return memoize(lambda: hook.add_effect(effect))
157159

@@ -181,29 +183,55 @@ def use_async_effect(
181183
dependencies: Sequence[Any] | ellipsis | None = ...,
182184
shutdown_timeout: float = 0.1,
183185
) -> Callable[[_AsyncEffectFunc], None] | None:
186+
"""
187+
A hook that manages an asynchronous side effect in a React-like component.
188+
189+
This hook allows you to run an asynchronous function as a side effect and
190+
ensures that the effect is properly cleaned up when the component is
191+
re-rendered or unmounted.
192+
193+
Args:
194+
function:
195+
Applies the effect and can return a clean-up function
196+
dependencies:
197+
Dependencies for the effect. The effect will only trigger if the identity
198+
of any value in the given sequence changes (i.e. their :func:`id` is
199+
different). By default these are inferred based on local variables that are
200+
referenced by the given function.
201+
shutdown_timeout:
202+
The amount of time (in seconds) to wait for the effect to complete before
203+
forcing a shutdown.
204+
205+
Returns:
206+
If not function is provided, a decorator. Otherwise ``None``.
207+
"""
184208
hook = current_hook()
185209
dependencies = _try_to_infer_closure_values(function, dependencies)
186210
memoize = use_memo(dependencies=dependencies)
187211
cleanup_func: Ref[_EffectCleanFunc | None] = use_ref(None)
188212

189213
def decorator(func: _AsyncEffectFunc) -> None:
190214
async def effect(stop: asyncio.Event) -> None:
191-
if cleanup_func.current:
192-
cleanup_func.current()
193-
cleanup_func.current = None
215+
# Since the effect is asynchronous, we need to make sure we
216+
# always clean up the previous effect's resources
217+
run_effect_cleanup(cleanup_func)
194218

195219
# Execute the effect in a background task
196220
task = asyncio.create_task(func())
197221

198-
# Wait until the effect is stopped
222+
# Wait until we get the signal to stop this effect
199223
await stop.wait()
200224

201-
# Try to fetch the results of the task
225+
# If renders are queued back-to-back, then this effect function might have
226+
# not completed. So, we give the task a small amount of time to finish.
227+
# If it manages to finish, we can obtain a clean-up function.
202228
results, _ = await asyncio.wait([task], timeout=shutdown_timeout)
203229
if results:
204230
cleanup_func.current = results.pop().result()
205-
if cleanup_func.current:
206-
cleanup_func.current()
231+
232+
# Run the clean-up function when the effect is stopped,
233+
# if it hasn't been run already by a new effect
234+
run_effect_cleanup(cleanup_func)
207235

208236
# Cancel the task if it's still running
209237
task.cancel()
@@ -584,3 +612,9 @@ def strictly_equal(x: Any, y: Any) -> bool:
584612

585613
# Fallback to identity check
586614
return x is y # pragma: no cover
615+
616+
617+
def run_effect_cleanup(cleanup_func: Ref[_EffectCleanFunc | None]) -> None:
618+
if cleanup_func.current:
619+
cleanup_func.current()
620+
cleanup_func.current = None

0 commit comments

Comments
 (0)