@@ -141,17 +141,19 @@ def use_effect(
141
141
142
142
def decorator (func : _SyncEffectFunc ) -> None :
143
143
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 )
147
147
148
148
# Execute the effect and store the clean-up function
149
149
cleanup_func .current = func ()
150
150
151
- # Run the clean-up function when the effect is stopped
151
+ # Wait until we get the signal to stop this effect
152
152
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 )
155
157
156
158
return memoize (lambda : hook .add_effect (effect ))
157
159
@@ -181,29 +183,55 @@ def use_async_effect(
181
183
dependencies : Sequence [Any ] | ellipsis | None = ...,
182
184
shutdown_timeout : float = 0.1 ,
183
185
) -> 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
+ """
184
208
hook = current_hook ()
185
209
dependencies = _try_to_infer_closure_values (function , dependencies )
186
210
memoize = use_memo (dependencies = dependencies )
187
211
cleanup_func : Ref [_EffectCleanFunc | None ] = use_ref (None )
188
212
189
213
def decorator (func : _AsyncEffectFunc ) -> None :
190
214
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 )
194
218
195
219
# Execute the effect in a background task
196
220
task = asyncio .create_task (func ())
197
221
198
- # Wait until the effect is stopped
222
+ # Wait until we get the signal to stop this effect
199
223
await stop .wait ()
200
224
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.
202
228
results , _ = await asyncio .wait ([task ], timeout = shutdown_timeout )
203
229
if results :
204
230
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 )
207
235
208
236
# Cancel the task if it's still running
209
237
task .cancel ()
@@ -584,3 +612,9 @@ def strictly_equal(x: Any, y: Any) -> bool:
584
612
585
613
# Fallback to identity check
586
614
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