3030
3131
3232__all__ = [
33+ "use_async_effect" ,
3334 "use_callback" ,
3435 "use_effect" ,
3536 "use_memo" ,
@@ -119,7 +120,12 @@ def use_effect(
119120 function : _SyncEffectFunc | None = None ,
120121 dependencies : Sequence [Any ] | ellipsis | None = ...,
121122) -> Callable [[_SyncEffectFunc ], None ] | None :
122- """See the full :ref:`Use Effect` docs for details
123+ """
124+ A hook that manages an synchronous side effect in a React-like component.
125+
126+ This hook allows you to run a synchronous function as a side effect and
127+ ensures that the effect is properly cleaned up when the component is
128+ re-rendered or unmounted.
123129
124130 Parameters:
125131 function:
@@ -136,96 +142,114 @@ def use_effect(
136142 hook = current_hook ()
137143 dependencies = _try_to_infer_closure_values (function , dependencies )
138144 memoize = use_memo (dependencies = dependencies )
139- last_clean_callback : Ref [_EffectCleanFunc | None ] = use_ref (None )
145+ cleanup_func : Ref [_EffectCleanFunc | None ] = use_ref (None )
140146
141- def add_effect ( function : _SyncEffectFunc ) -> None :
147+ def decorator ( func : _SyncEffectFunc ) -> None :
142148 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 ()
149+ # Since the effect is asynchronous, we need to make sure we
150+ # always clean up the previous effect's resources
151+ run_effect_cleanup (cleanup_func )
152+
153+ # Execute the effect and store the clean-up function
154+ cleanup_func .current = func ()
155+
156+ # Wait until we get the signal to stop this effect
147157 await stop .wait ()
148- if clean is not None :
149- clean ()
158+
159+ # Run the clean-up function when the effect is stopped,
160+ # if it hasn't been run already by a new effect
161+ run_effect_cleanup (cleanup_func )
150162
151163 return memoize (lambda : hook .add_effect (effect ))
152164
153- if function is not None :
154- add_effect (function )
165+ # Handle decorator usage
166+ if function :
167+ decorator (function )
155168 return None
156-
157- return add_effect
169+ return decorator
158170
159171
160172@overload
161173def use_async_effect (
162174 function : None = None ,
163175 dependencies : Sequence [Any ] | ellipsis | None = ...,
176+ shutdown_timeout : float = 0.1 ,
164177) -> Callable [[_EffectApplyFunc ], None ]: ...
165178
166179
167180@overload
168181def use_async_effect (
169182 function : _AsyncEffectFunc ,
170183 dependencies : Sequence [Any ] | ellipsis | None = ...,
184+ shutdown_timeout : float = 0.1 ,
171185) -> None : ...
172186
173187
174188def use_async_effect (
175189 function : _AsyncEffectFunc | None = None ,
176190 dependencies : Sequence [Any ] | ellipsis | None = ...,
191+ shutdown_timeout : float = 0.1 ,
177192) -> Callable [[_AsyncEffectFunc ], None ] | None :
178- """See the full :ref:`Use Effect` docs for details
193+ """
194+ A hook that manages an asynchronous side effect in a React-like component.
179195
180- Parameters:
196+ This hook allows you to run an asynchronous function as a side effect and
197+ ensures that the effect is properly cleaned up when the component is
198+ re-rendered or unmounted.
199+
200+ Args:
181201 function:
182202 Applies the effect and can return a clean-up function
183203 dependencies:
184204 Dependencies for the effect. The effect will only trigger if the identity
185205 of any value in the given sequence changes (i.e. their :func:`id` is
186206 different). By default these are inferred based on local variables that are
187207 referenced by the given function.
208+ shutdown_timeout:
209+ The amount of time (in seconds) to wait for the effect to complete before
210+ forcing a shutdown.
188211
189212 Returns:
190213 If not function is provided, a decorator. Otherwise ``None``.
191214 """
192215 hook = current_hook ()
193216 dependencies = _try_to_infer_closure_values (function , dependencies )
194217 memoize = use_memo (dependencies = dependencies )
195- last_clean_callback : Ref [_EffectCleanFunc | None ] = use_ref (None )
218+ cleanup_func : Ref [_EffectCleanFunc | None ] = use_ref (None )
196219
197- def add_effect (function : _AsyncEffectFunc ) -> None :
198- def sync_executor () -> _EffectCleanFunc | None :
199- task = asyncio .create_task (function ())
200-
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 ()
220+ def decorator (func : _AsyncEffectFunc ) -> None :
221+ async def effect (stop : asyncio .Event ) -> None :
222+ # Since the effect is asynchronous, we need to make sure we
223+ # always clean up the previous effect's resources
224+ run_effect_cleanup (cleanup_func )
210225
211- return clean_future
226+ # Execute the effect in a background task
227+ task = asyncio .create_task (func ())
212228
213- async def effect (stop : asyncio .Event ) -> None :
214- if last_clean_callback .current is not None :
215- last_clean_callback .current ()
216- last_clean_callback .current = None
217- clean = last_clean_callback .current = sync_executor ()
229+ # Wait until we get the signal to stop this effect
218230 await stop .wait ()
219- if clean is not None :
220- clean ()
231+
232+ # If renders are queued back-to-back, the effect might not have
233+ # completed. So, we give the task a small amount of time to finish.
234+ # If it manages to finish, we can obtain a clean-up function.
235+ results , _ = await asyncio .wait ([task ], timeout = shutdown_timeout )
236+ if results :
237+ cleanup_func .current = results .pop ().result ()
238+
239+ # Run the clean-up function when the effect is stopped,
240+ # if it hasn't been run already by a new effect
241+ run_effect_cleanup (cleanup_func )
242+
243+ # Cancel the task if it's still running
244+ task .cancel ()
221245
222246 return memoize (lambda : hook .add_effect (effect ))
223247
224- if function is not None :
225- add_effect (function )
248+ # Handle decorator usage
249+ if function :
250+ decorator (function )
226251 return None
227-
228- return add_effect
252+ return decorator
229253
230254
231255def use_debug_value (
@@ -595,3 +619,9 @@ def strictly_equal(x: Any, y: Any) -> bool:
595619
596620 # Fallback to identity check
597621 return x is y # pragma: no cover
622+
623+
624+ def run_effect_cleanup (cleanup_func : Ref [_EffectCleanFunc | None ]) -> None :
625+ if cleanup_func .current :
626+ cleanup_func .current ()
627+ cleanup_func .current = None
0 commit comments