11from __future__ import annotations
22
33import logging
4- from asyncio import gather
5- from collections .abc import Awaitable
6- from typing import Any , Callable , TypeVar
4+ from asyncio import Event , Task , create_task , gather
5+ from typing import Any , Callable , Protocol , TypeVar
76
87from anyio import Semaphore
98
1211
1312T = TypeVar ("T" )
1413
14+
15+ class EffectFunc (Protocol ):
16+ async def __call__ (self , stop : Event ) -> None :
17+ ...
18+
19+
1520logger = logging .getLogger (__name__ )
1621
1722_HOOK_STATE : ThreadLocal [list [LifeCycleHook ]] = ThreadLocal (list )
@@ -95,8 +100,9 @@ async def stop_effect():
95100 "__weakref__" ,
96101 "_context_providers" ,
97102 "_current_state_index" ,
98- "_effect_cleanups" ,
99- "_effect_startups" ,
103+ "_effect_funcs" ,
104+ "_effect_tasks" ,
105+ "_effect_stops" ,
100106 "_render_access" ,
101107 "_rendered_atleast_once" ,
102108 "_schedule_render_callback" ,
@@ -117,8 +123,9 @@ def __init__(
117123 self ._rendered_atleast_once = False
118124 self ._current_state_index = 0
119125 self ._state : tuple [Any , ...] = ()
120- self ._effect_startups : list [Callable [[], Awaitable [None ]]] = []
121- self ._effect_cleanups : list [Callable [[], Awaitable [None ]]] = []
126+ self ._effect_funcs : list [EffectFunc ] = []
127+ self ._effect_tasks : list [Task [None ]] = []
128+ self ._effect_stops : list [Event ] = []
122129 self ._render_access = Semaphore (1 ) # ensure only one render at a time
123130
124131 def schedule_render (self ) -> None :
@@ -138,19 +145,15 @@ def use_state(self, function: Callable[[], T]) -> T:
138145 self ._current_state_index += 1
139146 return result
140147
141- def add_effect (
142- self ,
143- start_effect : Callable [[], Awaitable [None ]],
144- clean_effect : Callable [[], Awaitable [None ]],
145- ) -> None :
148+ def add_effect (self , effect_func : EffectFunc ) -> None :
146149 """Add an effect to this hook
147150
148- Effects are started when the component is done renderig and cleaned up when the
149- component is removed from the layout. Any other actions (e.g. re-running the
150- effect if a dependency changes) are the responsibility of the effect itself.
151+ A task to run the effect is created when the component is done rendering.
152+ When the component will be unmounted, the event passed to the effect is
153+ triggered and the task is awaited. The effect should eventually halt after
154+ the event is triggered.
151155 """
152- self ._effect_startups .append (start_effect )
153- self ._effect_cleanups .append (clean_effect )
156+ self ._effect_funcs .append (effect_func )
154157
155158 def set_context_provider (self , provider : ContextProviderType [Any ]) -> None :
156159 self ._context_providers [provider .type ] = provider
@@ -176,24 +179,25 @@ async def affect_component_did_render(self) -> None:
176179
177180 async def affect_layout_did_render (self ) -> None :
178181 """The layout completed a render"""
179- try :
180- await gather (* [start () for start in self ._effect_startups ])
181- except Exception :
182- logger .exception ("Error during effect startup" )
183- finally :
184- self ._effect_startups .clear ()
185- if self ._schedule_render_later :
186- self ._schedule_render ()
187- self ._schedule_render_later = False
182+ stop = Event ()
183+ self ._effect_stops .append (stop )
184+ self ._effect_tasks .extend (create_task (e (stop )) for e in self ._effect_funcs )
185+ self ._effect_funcs .clear ()
186+ if self ._schedule_render_later :
187+ self ._schedule_render ()
188+ self ._schedule_render_later = False
188189
189190 async def affect_component_will_unmount (self ) -> None :
190191 """The component is about to be removed from the layout"""
192+ for stop in self ._effect_stops :
193+ stop .set ()
194+ self ._effect_stops .clear ()
191195 try :
192- await gather (* [ clean () for clean in self ._effect_cleanups ] )
196+ await gather (* self ._effect_tasks )
193197 except Exception :
194- logger .exception ("Error during effect cleanup " )
198+ logger .exception ("Error in effect" )
195199 finally :
196- self ._effect_cleanups .clear ()
200+ self ._effect_tasks .clear ()
197201
198202 def set_current (self ) -> None :
199203 """Set this hook as the active hook in this thread
0 commit comments