Skip to content

fix: use_effect() skipping cleanups and re-running setups before cleanups finish - #7005

Open
ndonkoHenri wants to merge 4 commits into
flet-1.1.0from
fix/use-effect-cleanup-follow-up
Open

ndonkoHenri wants to merge 4 commits into
flet-1.1.0from
fix/use-effect-cleanup-follow-up

Conversation

@ndonkoHenri

@ndonkoHenri ndonkoHenri commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #6983, which fixed #6963: a cleanup returned by a use_effect setup was lost once the component re-rendered.

What was still wrong

  • Passing cleanup= as well as returning a cleanup ran only one of them.
  • async cleanups never ran on a dependency change. The cleanup started a task, then the setup that followed cancelled the effect's tasks, including the cleanup it had just started.
  • Order wasn't guaranteed. An async cleanup could still be running when the setup ran again. For example, opening a new connection while the old one was still flushing and closing.

What this does

An effect's cleanups now always finish, in order, before its setup runs again.

  • Order: the cleanup the setup returned runs first, then cleanup=. Non-async cleanups run right away. Once an async one starts, the rest finish in a task.
  • Waiting: while that task runs, the effect's later setups and cleanups wait in a per-effect queue. The queue runs as soon as the task is done.
    • A setup that was superseded while waiting is skipped.
    • None runs after the component unmounts, or after the session closes or disconnects.
  • One setup per render: effect requests carry the setup function they were scheduled with, so each render's setup runs once, even when renders pile up quickly.
  • One cleanup per setup: cleanups run only if a setup ran since the last ones. A setup on an effect that's still active runs its cleanups first. A setup that runs after its component unmounted is cleaned up right away, which keeps on_unmounted working when a component unmounts before its effects ran.
  • Errors: one failing cleanup doesn't keep the others from running, and errors go to the session's error report. A CancelledError raised by an effect counts as cancelled, not as an error.
  • Garbage collection: cleanup tasks are held by the session until they finish, so they aren't collected while pending.

An async setup's return value still isn't used as a cleanup. The use_effect docstring says so, and points to cleanup= instead.

Docs

The use_effect docstring now explains the lifecycle, and that a returned cleanup sees its own setup's values while cleanup= is the latest render's function. It also gains an example that returns a cleanup. on_updated now says it also runs after mount, which it always did.

Tests

The effect tests move into tests/test_use_effect.py:

  • TestEffectScheduling checks what a component queues, with _schedule_effect stubbed.
  • TestEffectLoop drives effects end-to-end through a real Session effect loop.

The tests #6983 added in test_component_effects.py and test_session_disconnect_buffering.py are folded in or covered by the new ones. test_component_effects.py, left with a single unmount test, becomes test_component_unmount.py.

Testing

  • Python 3.13: test_use_effect.py (31 tests) passed over repeat runs with -W error. The full flet suite: 461 passed, 9 skipped.
  • Python 3.10: the effect test files pass. The Task.cancelling() test is skipped, since it needs 3.11.
  • Against the old code: the new tests for this behavior fail on flet-1.1.0.
  • In a real app: the docstring's clock example ran with flet run --web. It ticked, re-rendered every second, and ran its returned cleanup exactly once per unmount, over two show/hide cycles.

Summary by Sourcery

Make use_effect() reliably complete each effect's cleanups before running replacement setups across synchronous, asynchronous, rerender, unmount, and session lifecycle transitions.

Bug Fixes:

  • Ensure use_effect() cleanups run exactly once and in order, including both setup-returned and cleanup= callbacks, before a dependent setup runs again.
  • Complete asynchronous cleanups before subsequent effect work, while cancelling obsolete setups and suppressing queued work after unmount or session closure.
  • Report cleanup failures independently so one error does not prevent remaining cleanups from running, while treating cancellation as non-error behavior.

Enhancements:

  • Preserve the setup function associated with each scheduled render and coalesce superseded queued setups to maintain predictable effect lifecycle behavior.
  • Retain pending asynchronous cleanup tasks until completion.

Documentation:

  • Expand use_effect, on_mounted, on_unmounted, and on_updated documentation with lifecycle semantics, cleanup behavior, and a usage example.

Tests:

  • Consolidate effect coverage into scheduling and end-to-end lifecycle tests, including asynchronous ordering, cancellation, errors, unmounting, disconnection, queuing, and garbage collection.

Chores:

  • Update the changelog and separate the remaining component unmount test from effect tests.

…lled

When a setup returned a cleanup and `cleanup=` was passed too, only one of
them ran. The returned cleanup now runs first, then the argument, each
reported on its own if it raises so the other still runs.

A dependency change queued the cleanup and then the setup, and the setup
cancelled the effect's tasks, killing the async cleanup it had just
started. Only a still-running setup task is cancelled now.

The effect tests move into test_use_effect.py, grouped into scheduling and
end-to-end effect loop tests, and test_component_effects.py, left with an
unmount test, becomes test_component_unmount.py.
An effect's cleanups now always finish, in order, before its setup runs
again. Non-async cleanups run right away; once an async one starts, the
rest finish in a task, and the effect's later setups and cleanups wait in
a per-effect queue that runs as soon as the task is done. Superseded
setups are skipped, and none runs after the component unmounts or the
session closes or disconnects.

Effect requests carry the setup function they were scheduled with, so
each render's setup runs once. Cleanups run only if a setup ran since the
last ones, a setup on an active effect runs its cleanups first, and a
setup that runs after its component unmounted is cleaned up right away.

Cleanup tasks are held by the session until they finish, so they aren't
garbage collected while pending. A CancelledError raised by an effect
counts as cancelled rather than failed, and one failing cleanup doesn't
keep the others from running.
The use_effect docstring now explains when the setup and cleanups run,
that cleanups always finish in order before the setup runs again, and
that a returned cleanup sees its own setup's values while `cleanup` is
the latest render's function. It gains an example that returns a
cleanup.

on_updated says it also runs after the component mounts, and the
on_mounted, on_unmounted and on_updated docstrings drop the repeated
types and note that `fn` may be async.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying flet-website-v2 with  Cloudflare Pages  Cloudflare Pages

Latest commit: 71c4510
Status: ✅  Deploy successful!
Preview URL: https://274698ab.flet-website-v2.pages.dev
Branch Preview URL: https://fix-use-effect-cleanup-follo.flet-website-v2.pages.dev

View logs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant