1
1
from __future__ import annotations
2
2
3
3
import asyncio
4
+ from asyncio import Event , Task , create_task
5
+ from inspect import iscoroutinefunction , isfunction , signature
4
6
from logging import getLogger
5
7
from types import FunctionType
6
8
from typing import (
7
9
TYPE_CHECKING ,
8
10
Any ,
9
- Awaitable ,
10
11
Callable ,
11
12
Generic ,
12
13
NewType ,
15
16
cast ,
16
17
overload ,
17
18
)
19
+ from warnings import warn
18
20
19
- from typing_extensions import Protocol , TypeAlias
21
+ from typing_extensions import Literal , Protocol , TypeAlias , TypedDict
20
22
21
23
from reactpy .config import REACTPY_DEBUG_MODE
22
24
from reactpy .utils import Ref
23
25
24
26
from ._thread_local import ThreadLocal
25
- from .types import ComponentType , Key , State , VdomDict
27
+ from .types import AsyncEffect , ComponentType , Key , State , SyncEffect , VdomDict
26
28
27
29
28
30
if not TYPE_CHECKING :
@@ -96,32 +98,26 @@ def dispatch(new: _Type | Callable[[_Type], _Type]) -> None:
96
98
self .dispatch = dispatch
97
99
98
100
99
- _EffectCleanFunc : TypeAlias = "Callable[[], None]"
100
- _SyncEffectFunc : TypeAlias = "Callable[[], _EffectCleanFunc | None]"
101
- _AsyncEffectFunc : TypeAlias = "Callable[[], Awaitable[_EffectCleanFunc | None]]"
102
- _EffectApplyFunc : TypeAlias = "_SyncEffectFunc | _AsyncEffectFunc"
103
-
104
-
105
101
@overload
106
102
def use_effect (
107
103
function : None = None ,
108
104
dependencies : Sequence [Any ] | ellipsis | None = ...,
109
- ) -> Callable [[_EffectApplyFunc ], None ]:
105
+ ) -> Callable [[SyncEffect | AsyncEffect ], None ]:
110
106
...
111
107
112
108
113
109
@overload
114
110
def use_effect (
115
- function : _EffectApplyFunc ,
111
+ function : SyncEffect | AsyncEffect ,
116
112
dependencies : Sequence [Any ] | ellipsis | None = ...,
117
113
) -> None :
118
114
...
119
115
120
116
121
117
def use_effect (
122
- function : _EffectApplyFunc | None = None ,
118
+ function : SyncEffect | AsyncEffect | None = None ,
123
119
dependencies : Sequence [Any ] | ellipsis | None = ...,
124
- ) -> Callable [[_EffectApplyFunc ], None ] | None :
120
+ ) -> Callable [[SyncEffect | AsyncEffect ], None ] | None :
125
121
"""See the full :ref:`Use Effect` docs for details
126
122
127
123
Parameters:
@@ -140,42 +136,65 @@ def use_effect(
140
136
141
137
dependencies = _try_to_infer_closure_values (function , dependencies )
142
138
memoize = use_memo (dependencies = dependencies )
143
- last_clean_callback : Ref [_EffectCleanFunc | None ] = use_ref (None )
139
+ state : _EffectState = _use_const (
140
+ lambda : {"prior_task" : None , "prior_callback" : None }
141
+ )
144
142
145
- def add_effect (function : _EffectApplyFunc ) -> None :
146
- if not asyncio .iscoroutinefunction (function ):
147
- sync_function = cast (_SyncEffectFunc , function )
148
- else :
149
- async_function = cast (_AsyncEffectFunc , function )
143
+ def add_effect (function : SyncEffect | AsyncEffect ) -> None :
144
+ memoize (lambda : _add_effect (hook , state , function ))
145
+ return None
150
146
151
- def sync_function () -> _EffectCleanFunc | None :
152
- future = asyncio .ensure_future (async_function ())
147
+ if function is not None :
148
+ add_effect (function )
149
+ return None
150
+ else :
151
+ return add_effect
153
152
154
- def clean_future () -> None :
155
- if not future .cancel ():
156
- clean = future .result ()
157
- if clean is not None :
158
- clean ()
159
153
160
- return clean_future
154
+ def _add_effect (
155
+ hook : LifeCycleHook , state : _EffectState , function : SyncEffect | AsyncEffect
156
+ ) -> None :
157
+ sync_function : SyncEffect
158
+
159
+ if iscoroutinefunction (function ):
160
+ if not signature (function ).parameters : # pragma: no cover
161
+ warn (
162
+ f"Async effect functions { function } should accept two arguments - the "
163
+ "prior task and an interrupt event. This will be required in a future "
164
+ "release." ,
165
+ DeprecationWarning ,
166
+ )
167
+ original_function = function
161
168
162
- def effect () -> None :
163
- if last_clean_callback .current is not None :
164
- last_clean_callback .current ()
169
+ def function (prior_task : Task | None , _ : Event ) -> None :
170
+ if prior_task is not None :
171
+ prior_task .cancel ()
172
+ return original_function ()
165
173
166
- clean = last_clean_callback .current = sync_function ()
167
- if clean is not None :
168
- hook .add_effect (COMPONENT_WILL_UNMOUNT_EFFECT , clean )
174
+ def sync_function () -> Callable [[], None ]:
175
+ interupt = Event ()
176
+ state ["prior_task" ] = create_task (function (state ["prior_task" ], interupt ))
177
+ return interupt .set
169
178
170
- return None
179
+ elif isfunction (function ):
180
+ sync_function = function
181
+ else :
182
+ raise TypeError (f"Expected a function, not { function !r} " )
171
183
172
- return memoize (lambda : hook .add_effect (LAYOUT_DID_RENDER_EFFECT , effect ))
184
+ def effect () -> None :
185
+ prior_callback = state ["prior_callback" ]
186
+ if prior_callback is not None :
187
+ prior_callback ()
188
+ next_callback = state ["prior_callback" ] = sync_function ()
189
+ if next_callback is not None :
190
+ hook .add_effect (COMPONENT_WILL_UNMOUNT_EFFECT , next_callback )
173
191
174
- if function is not None :
175
- add_effect (function )
176
- return None
177
- else :
178
- return add_effect
192
+ hook .add_effect (LAYOUT_DID_RENDER_EFFECT , effect )
193
+
194
+
195
+ class _EffectState (TypedDict ):
196
+ prior_task : Task | None
197
+ prior_callback : Callable [[], None ] | None
179
198
180
199
181
200
def use_debug_value (
0 commit comments