Skip to content

fix(bento): dispose homepage animation listeners and timers on unmount - #3168

Open
WhoamiI00 wants to merge 2 commits into
appwrite:mainfrom
WhoamiI00:fix/bento-animation-cleanup
Open

fix(bento): dispose homepage animation listeners and timers on unmount#3168
WhoamiI00 wants to merge 2 commits into
appwrite:mainfrom
WhoamiI00:fix/bento-animation-cleanup

Conversation

@WhoamiI00

Copy link
Copy Markdown

What does this PR do?

The bento animation components on the homepage register motion's hover() and inView() handlers inside $effect / onMount, but discard the disposer function each one returns and never cancel the in-flight animation. Nothing is torn down when the component is destroyed by a client-side navigation.

This has two visible consequences:

1. Every bento card leaks its IntersectionObserver. Navigating away from the homepage left 10 of 12 observers alive, each still holding a reference to the now-detached subtree.

2. auth.svelte throws an uncaught error. Hovering the Auth card starts a 1000ms write() interval. Because the card is a link, clicking it mid-animation is a completely ordinary interaction — the page navigates, the component is destroyed, but the interval keeps running and its .then() still calls animate(button, ...) after bind:this has reset button to null:

Error: You're trying to perform an animation on null. Ensure that selectors are
correctly finding elements and refs are correctly hydrated.
    at animateSubject (motion.js)
    at bento/(animations)/auth.svelte

This reproduces on production today — on appwrite.io the minified build surfaces it as Invalid value used as weak map key from the same call.

sites.svelte has the same shape and additionally leaves a 44-second animate() running after unmount.

The fix

Capture the disposers that hover() / inView() already return, return a teardown from the effect, and stop any in-flight animation. auth.svelte also guards the deferred button pulse, since a write() that settles at the exact moment of unmount resolves after teardown has run and so cannot be cancelled.

No behaviour changes while the component is mounted — this only adds cleanup.

Test Plan

Repro (before this PR): load the homepage, hover the Auth bento card, and click it within ~1s. The error above appears in the console on /products/auth.

Measured with an instrumented Playwright run that hovers the Auth card, clicks through to /products/auth, then counts animation ticks that still fire and observers that are still connected:

before after
animation ticks after unmount 11 0
IntersectionObservers still live 10 3
uncaught error on navigation yes no

The 7 newly-disposed observers correspond exactly to the 7 bento cards; the remaining 3 belong to other components on the page and are out of scope here.

Also verified there is no regression to the hover animation itself — rapid hover in/out over 12 cycles still shows 1 concurrent animation, 0 multi-character jumps, and clean write/unwrite alternation, identical to main.

Checks: svelte-check reports 0 errors and 0 warnings, and prettier --check passes on all touched files.

Related PRs and Issues

None. Found while investigating #2758 (which is already fixed by d1b92c9 — I've left a note on that issue).

Have you read the Contributing Guidelines on issues?

Yes.

The bento animation components register motion `hover()` and `inView()`
handlers inside `$effect`/`onMount` but discard the disposer each one
returns, and never cancel the in-flight animation. Nothing is torn down
when the component is destroyed by a client-side navigation.

Two consequences:

- Every card leaks its IntersectionObserver. Leaving the homepage left
  10 of 12 observers alive, holding a reference to the detached subtree.
- `auth.svelte` starts a 1000ms `write()` interval on hover. Clicking the
  card mid-animation navigates away, but the interval keeps running and
  its `.then()` still fires `animate(button, ...)` after `bind:this` has
  reset `button` to null, throwing "You're trying to perform an animation
  on null". `sites.svelte` similarly leaves a 44s `animate()` running.

Capture the disposers, return a teardown from the effect, and stop any
in-flight animation. `auth.svelte` also guards the deferred button pulse,
since a write that settles exactly as the component unmounts resolves
after teardown has already run.

Measured on the homepage, hovering the Auth card then clicking it:

                                 before   after
  animation ticks after unmount      11       0
  IntersectionObservers still live   10       3

The 7 disposed observers are exactly the 7 bento cards; the remaining 3
belong to other components on the page.
@greptile-apps

greptile-apps Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds lifecycle cleanup to the homepage bento animations so client-side navigation no longer leaves their Motion handlers and long-running animation work active.

  • Retains and invokes hover() and inView() disposers across seven bento cards.
  • Cancels Auth typing work and guards its deferred button pulse after unmount.
  • Stops tracked Functions and Sites animations during teardown.

Confidence Score: 4/5

The PR is safe to merge, with a non-blocking cleanup gap for short-lived animations in four bento cards.

Listener and observer disposal is correctly added, and the stateful or long-running Auth, Functions, and Sites animations are explicitly cancelled; several other cards still allow already-started element animations to finish after destruction.

Files Needing Attention: src/routes/(marketing)/(components)/bento/(animations)/databases.svelte, messaging.svelte, realtime.svelte, and storage.svelte

Important Files Changed

Filename Overview
src/routes/(marketing)/(components)/bento/(animations)/auth.svelte Disposes hover and viewport handlers, cancels typing intervals, and guards the deferred button pulse after unmount.
src/routes/(marketing)/(components)/bento/(animations)/databases.svelte Disposes hover and viewport handlers, but already-started Motion animations remain untracked during teardown.
src/routes/(marketing)/(components)/bento/(animations)/functions.svelte Disposes handlers and stops the tracked width animation during teardown.
src/routes/(marketing)/(components)/bento/(animations)/messaging.svelte Disposes handlers while leaving already-started element animations to finish on detached DOM.
src/routes/(marketing)/(components)/bento/(animations)/realtime.svelte Disposes handlers while leaving already-started element animations to finish on detached DOM.
src/routes/(marketing)/(components)/bento/(animations)/sites.svelte Disposes handlers and stops the tracked seconds and text animations during teardown.
src/routes/(marketing)/(components)/bento/(animations)/storage.svelte Disposes handlers while leaving already-started element animations to finish on detached DOM.

Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
### Issue 1
src/routes/(marketing)/(components)/bento/(animations)/databases.svelte:140-143
**Active animations survive teardown**

`stopHover()` and `stopInView()` only detach Motion's event and observer handlers; they do not stop animations already started by their callbacks. Navigating away during an animation therefore leaves work running against the detached card until completion; retain and stop the animation controls here and in the equivalent messaging, realtime, and storage cleanup paths.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(bento): dispose homepage animation l..." | Re-trigger Greptile

Comment on lines +140 to +143

return () => {
stopHover();
stopInView();

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.

P2 Active animations survive teardown

stopHover() and stopInView() only detach Motion's event and observer handlers; they do not stop animations already started by their callbacks. Navigating away during an animation therefore leaves work running against the detached card until completion; retain and stop the animation controls here and in the equivalent messaging, realtime, and storage cleanup paths.

Knowledge Base Used: Marketing pages

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/routes/(marketing)/(components)/bento/(animations)/databases.svelte
Line: 140-143

Comment:
**Active animations survive teardown**

`stopHover()` and `stopInView()` only detach Motion's event and observer handlers; they do not stop animations already started by their callbacks. Navigating away during an animation therefore leaves work running against the detached card until completion; retain and stop the animation controls here and in the equivalent messaging, realtime, and storage cleanup paths.

**Knowledge Base Used:** [Marketing pages](https://app.greptile.com/appwrite/-/custom-context/knowledge-base/appwrite/website/-/docs/marketing-pages.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Valid, and fixed in f2f5c3a — thanks.

You're right that the returned cleanups only detach the pointer listener and the observer. What made this worth fixing is that the PR was already inconsistent about it: auth, functions and sites do hold their controls and stop them on teardown, while databases, messaging, realtime and storage discarded the return value of animate() entirely. So the four you flagged were the gap, and the fix just brings them in line with the three that were already right.

Each of those four animates a single element and a new animation supersedes the previous one on it, so holding the latest controls is sufficient — no set or bookkeeping needed:

let animation: ReturnType<typeof animate> | undefined;
...
animation = animate(table, { ... }, { ... });
...
return () => {
    stopHover();
    stopInView();
    animation?.stop();
};

I used stop() rather than cancel() deliberately: stop() halts at the current state, while cancel() applies the initial state, which would visibly snap the card back if the effect re-ran rather than unmounted.

Typed via ReturnType<typeof animate> because AnimationPlaybackControlsWithThen is imported from motion-dom inside framer-motion's dom.d.ts rather than re-exported from motion, so naming it directly would reach past the public entry point.

svelte-check is clean — 0 errors, 0 warnings.

`hover()` and `inView()` return cleanups that detach the pointer listener
and the observer, but they do not touch an animation those callbacks have
already started. Leaving the page mid-transition left that animation
running against a card no longer in the document.

`auth`, `functions` and `sites` already held their controls and stopped
them; `databases`, `messaging`, `realtime` and `storage` discarded the
return value of `animate()` and only detached the handlers. They now keep
the latest controls and stop them alongside, which is all that is needed
since a new animation on the same element supersedes the previous one.

`stop()` halts at the current state rather than `cancel()`, which would
snap the card back to the initial state if the effect re-ran.
@appwrite

appwrite Bot commented Aug 27, 2026

Copy link
Copy Markdown

Appwrite Website

Project ID: 69d7efb00023389e8d27

Sites (1)
Site Status Logs Preview QR
 website
69d7f2670014e24571ca
Queued Queued Authorize Preview URL QR Code

Website (appwrite/website)

Project ID: 684969cb000a2f6c0a02

Sites (1)
Site Status Logs Preview QR
 website
68496a17000f03d62013
Queued Queued Authorize Preview URL QR Code


Tip

Custom domains work with both CNAME for subdomains and NS records for apex domains

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