30
30
31
31
32
32
__all__ = [
33
+ "use_async_effect" ,
33
34
"use_callback" ,
34
35
"use_effect" ,
35
36
"use_memo" ,
@@ -119,7 +120,12 @@ def use_effect(
119
120
function : _SyncEffectFunc | None = None ,
120
121
dependencies : Sequence [Any ] | ellipsis | None = ...,
121
122
) -> 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.
123
129
124
130
Parameters:
125
131
function:
@@ -136,96 +142,114 @@ def use_effect(
136
142
hook = current_hook ()
137
143
dependencies = _try_to_infer_closure_values (function , dependencies )
138
144
memoize = use_memo (dependencies = dependencies )
139
- last_clean_callback : Ref [_EffectCleanFunc | None ] = use_ref (None )
145
+ cleanup_func : Ref [_EffectCleanFunc | None ] = use_ref (None )
140
146
141
- def add_effect ( function : _SyncEffectFunc ) -> None :
147
+ def decorator ( func : _SyncEffectFunc ) -> None :
142
148
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
147
157
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 )
150
162
151
163
return memoize (lambda : hook .add_effect (effect ))
152
164
153
- if function is not None :
154
- add_effect (function )
165
+ # Handle decorator usage
166
+ if function :
167
+ decorator (function )
155
168
return None
156
-
157
- return add_effect
169
+ return decorator
158
170
159
171
160
172
@overload
161
173
def use_async_effect (
162
174
function : None = None ,
163
175
dependencies : Sequence [Any ] | ellipsis | None = ...,
176
+ shutdown_timeout : float = 0.1 ,
164
177
) -> Callable [[_EffectApplyFunc ], None ]: ...
165
178
166
179
167
180
@overload
168
181
def use_async_effect (
169
182
function : _AsyncEffectFunc ,
170
183
dependencies : Sequence [Any ] | ellipsis | None = ...,
184
+ shutdown_timeout : float = 0.1 ,
171
185
) -> None : ...
172
186
173
187
174
188
def use_async_effect (
175
189
function : _AsyncEffectFunc | None = None ,
176
190
dependencies : Sequence [Any ] | ellipsis | None = ...,
191
+ shutdown_timeout : float = 0.1 ,
177
192
) -> 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.
179
195
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:
181
201
function:
182
202
Applies the effect and can return a clean-up function
183
203
dependencies:
184
204
Dependencies for the effect. The effect will only trigger if the identity
185
205
of any value in the given sequence changes (i.e. their :func:`id` is
186
206
different). By default these are inferred based on local variables that are
187
207
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.
188
211
189
212
Returns:
190
213
If not function is provided, a decorator. Otherwise ``None``.
191
214
"""
192
215
hook = current_hook ()
193
216
dependencies = _try_to_infer_closure_values (function , dependencies )
194
217
memoize = use_memo (dependencies = dependencies )
195
- last_clean_callback : Ref [_EffectCleanFunc | None ] = use_ref (None )
218
+ cleanup_func : Ref [_EffectCleanFunc | None ] = use_ref (None )
196
219
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 )
210
225
211
- return clean_future
226
+ # Execute the effect in a background task
227
+ task = asyncio .create_task (func ())
212
228
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
218
230
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 ()
221
245
222
246
return memoize (lambda : hook .add_effect (effect ))
223
247
224
- if function is not None :
225
- add_effect (function )
248
+ # Handle decorator usage
249
+ if function :
250
+ decorator (function )
226
251
return None
227
-
228
- return add_effect
252
+ return decorator
229
253
230
254
231
255
def use_debug_value (
@@ -595,3 +619,9 @@ def strictly_equal(x: Any, y: Any) -> bool:
595
619
596
620
# Fallback to identity check
597
621
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