-
-
Notifications
You must be signed in to change notification settings - Fork 318
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
better async effects #1169
base: main
Are you sure you want to change the base?
better async effects #1169
Conversation
Will review on Monday. Not home at the moment. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the flexibility of generator approach a lot more than our previously proposed stop_event
interface.
But I do still think safeguards (timeout
) need to be put in place for programmer error. We can't trust everyone to right perfect code, so we need to detect, warn, then mitigate programming flaws.
Also, now that our async function interface has deviated wildly from the sync equivalent, it's starting to become clear to me we shouldn't overload use_effect
for async support any longer. We need to migrate towards a use_async_effect
hook.
If you never ``yield`` control back to ReactPy, then the component will never | ||
re-render and the effect will never be cleaned up. This is a common mistake when | ||
using ``use_effect`` for the first time. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like the idea of having no safeguard against this. An interface that doesn't protect against critical breaking behavior is usually the wrong interface.
My intuition has been telling me we probably want a new use_async_effect
hook that incorporates a timeout
parameter. Since ReactJS useEffect
only supports sync effects, it's currently an API mismatch that we're overloading use_effect
with async support.
Here, a new connection will be established whenever a new ``url`` is set. | ||
|
||
.. warning:: | ||
|
||
A component will be unable to render until all its outstanding effects have been |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
render -> re-render
or an async generator. If your effect doesn't need to do any cleanup, then you can | ||
simply write an async function. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
then you can simply write
-> then you should use
self._effect_tasks.extend(create_task(e(stop)) for e in self._effect_funcs) | ||
for effect_func in self._effect_funcs: | ||
effect_task = create_task(effect_func(stop)) | ||
# potential memory leak if the task doesn't remove itself when complete |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment needs a bit more description.
Is the memory leak due to our internal design, or due to user failure? Is the next LOC designed to prevent this issue, or is it the cause of this issue?
# potential memory leak if the task doesn't remove itself when complete | ||
effect_task.add_done_callback(lambda t: self._effect_tasks.remove(t)) | ||
self._effect_tasks.add(effect_task) | ||
self._effect_tasks.update() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any reason to call update
here?
def add_effect(function: _Effect) -> None: | ||
effect_func = _cast_async_effect(function) | ||
|
||
async def run_effect(stop: Event) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this function really need to be dynamically generated within add_effect
? It would be more memory efficient to move run_effect
outside of add_effect
, and pass in all needed values via args.
Dynamically defined functions are generally a bad idea for any long-running Python code due to memory usage.
f"Async effect {async_function_effect} returned a cleanup " | ||
f"function - use an async generator instead by yielding inside " | ||
"a try/finally block. This will be an error in a future " | ||
"version of ReactPy.", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to be mentioned in the changelog.
Alternatively to the generator style of cleanup, you can return a cleanup function from | ||
your effect function. As with the generator style, the cleanup function will be run | ||
before the component is unmounted or before the next effect is triggered when the | ||
component re-renders: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fact that returning cleanup functions will continue being allowed on sync effects (but not async effects) really makes me think we should commit to a use_async_effect
hook.
It feels confusing to mandate two completely separate user interfaces within the same hook.
I think we can have |
Django has a similar timeout design for views. They have an arbitrary default of 20 seconds. We just have to pick a "good enough" default, and trust the user to change it when needed. |
On that note, maybe |
Ultimately we have to trust the user to know what they're doing in some regard. Either we trust them to understand that if their effects don't end in a timely manner it will impact the responsiveness of components, or we trust that they understand how premature timeouts may cause memory leaks and unexpected behavior when resources aren't cleaned up. My intuition is that debugging issues with responsiveness will be easier than trying to find the source of memory leaks and orphaned resources. |
In the scenario where we auto-teardown, we've caught the error and can log the LOC. But in the scenario of user failure, it's a complete freeze with no user information. Maybe we can default it to something large like 20 seconds? At that point we'd be 98% confident it's a programming failure and worth an |
I'm realizing sync effects can be generators too. Maybe we can stick with one |
In terms of whether we diverge to creating In my mind, threading should be unique to sync calls. |
Someone already thought of these issues and developed something relatively clean in the ReactJS space. |
By submitting this pull request you agree that all contributions to this project are made under the MIT license.
Issues
Closes: #956
Solution
Effects can now be defines as sync or async generators (more info in the docs):
Note: this contains a subset of the changes originally created in #1093
Checklist
changelog.rst
has been updated with any significant changes.