@@ -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