From 63cdd29166014c6109e4d77dca8d8fd64260cc9e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 6 Dec 2024 15:33:49 -0500 Subject: [PATCH 1/5] feat: add `onFrame` lifecycle function --- .changeset/tasty-kangaroos-greet.md | 5 +++++ packages/svelte/src/index-client.js | 26 ++++++++++++++++++++++++++ packages/svelte/types/index.d.ts | 8 ++++++++ 3 files changed, 39 insertions(+) create mode 100644 .changeset/tasty-kangaroos-greet.md diff --git a/.changeset/tasty-kangaroos-greet.md b/.changeset/tasty-kangaroos-greet.md new file mode 100644 index 000000000000..07b6efa2be1c --- /dev/null +++ b/.changeset/tasty-kangaroos-greet.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: add `onFrame` lifecycle function diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 587d76623331..34a9694a75fe 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -161,6 +161,32 @@ export function afterUpdate(fn) { init_update_callbacks(component_context).a.push(fn); } +/** + * The `onFrame` function schedules a callback to run on `requestAnimationFrame`. It must be called inside an effect (e.g. during component initialisation). + * + * `onFrame` does not run inside [server-side components](https://svelte.dev/docs/svelte/svelte-server#render). + * + * @template T + * @param {() => NotFunction | Promise> | (() => any)} fn + * @returns {void} + */ +export function onFrame(fn) { + onMount(() => { + let frame = -1; + + function next() { + frame = requestAnimationFrame(next); + fn(); + } + + next(); + + return () => { + cancelAnimationFrame(frame); + }; + }); +} + /** * Legacy-mode: Init callbacks object for onMount/beforeUpdate/afterUpdate * @param {ComponentContext} context diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index f6b5b21f8085..c7eb847c010e 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -408,6 +408,13 @@ declare module 'svelte' { * @deprecated Use [`$effect`](https://svelte.dev/docs/svelte/$effect) instead * */ export function afterUpdate(fn: () => void): void; + /** + * The `onFrame` function schedules a callback to run on `requestAnimationFrame`. It must be called inside an effect (e.g. during component initialisation). + * + * `onFrame` does not run inside [server-side components](https://svelte.dev/docs/svelte/svelte-server#render). + * + * */ + export function onFrame(fn: () => NotFunction | Promise> | (() => any)): void; /** * Synchronously flushes any pending state changes and those that result from it. * */ @@ -1671,6 +1678,7 @@ declare module 'svelte/motion' { * * * ``` + * @since 5.8.0 */ export class Spring { constructor(value: T, options?: SpringOpts); From 00601986e5ed42d669bbcbe69b6ec83fa4fc6536 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 6 Dec 2024 15:40:30 -0500 Subject: [PATCH 2/5] noop on server --- packages/svelte/src/index-server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte/src/index-server.js b/packages/svelte/src/index-server.js index 0f1aff8f5aa7..af22f25d7f51 100644 --- a/packages/svelte/src/index-server.js +++ b/packages/svelte/src/index-server.js @@ -12,6 +12,7 @@ export function onDestroy(fn) { export { noop as beforeUpdate, noop as afterUpdate, + noop as onFrame, noop as onMount, noop as flushSync, run as untrack From fbf247d26801977516dbc1fa1803d4e57ad2b87e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 11 Apr 2025 21:37:09 -0400 Subject: [PATCH 3/5] rename to onAnimationFrame --- packages/svelte/src/index-client.js | 6 +++--- packages/svelte/src/index-server.js | 2 +- packages/svelte/types/index.d.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index ff0282fb2cca..92e35ac558b5 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -199,15 +199,15 @@ export function afterUpdate(fn) { } /** - * The `onFrame` function schedules a callback to run on `requestAnimationFrame`. It must be called inside an effect (e.g. during component initialisation). + * The `onAnimationFrame` function schedules a callback to run on `requestAnimationFrame`. It must be called inside an effect (e.g. during component initialisation). * - * `onFrame` does not run inside [server-side components](https://svelte.dev/docs/svelte/svelte-server#render). + * `onAnimationFrame` does not run inside [server-side components](https://svelte.dev/docs/svelte/svelte-server#render). * * @template T * @param {() => NotFunction | Promise> | (() => any)} fn * @returns {void} */ -export function onFrame(fn) { +export function onAnimationFrame(fn) { onMount(() => { let frame = -1; diff --git a/packages/svelte/src/index-server.js b/packages/svelte/src/index-server.js index af22f25d7f51..2fc726617ce4 100644 --- a/packages/svelte/src/index-server.js +++ b/packages/svelte/src/index-server.js @@ -12,7 +12,7 @@ export function onDestroy(fn) { export { noop as beforeUpdate, noop as afterUpdate, - noop as onFrame, + noop as onAnimationFrame, noop as onMount, noop as flushSync, run as untrack diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index c859a7d7939f..bc1ce3ca980d 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -411,12 +411,12 @@ declare module 'svelte' { * */ export function afterUpdate(fn: () => void): void; /** - * The `onFrame` function schedules a callback to run on `requestAnimationFrame`. It must be called inside an effect (e.g. during component initialisation). + * The `onAnimationFrame` function schedules a callback to run on `requestAnimationFrame`. It must be called inside an effect (e.g. during component initialisation). * - * `onFrame` does not run inside [server-side components](https://svelte.dev/docs/svelte/svelte-server#render). + * `onAnimationFrame` does not run inside [server-side components](https://svelte.dev/docs/svelte/svelte-server#render). * * */ - export function onFrame(fn: () => NotFunction | Promise> | (() => any)): void; + export function onAnimationFrame(fn: () => NotFunction | Promise> | (() => any)): void; /** * Create a snippet programmatically * */ From 605a1f732cf9a1c8f3bdedb8cd7c0f57b2cb2b26 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 11 Apr 2025 21:38:07 -0400 Subject: [PATCH 4/5] update changeset --- .changeset/tasty-kangaroos-greet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tasty-kangaroos-greet.md b/.changeset/tasty-kangaroos-greet.md index 07b6efa2be1c..407057155c56 100644 --- a/.changeset/tasty-kangaroos-greet.md +++ b/.changeset/tasty-kangaroos-greet.md @@ -2,4 +2,4 @@ 'svelte': minor --- -feat: add `onFrame` lifecycle function +feat: add `onAnimationFrame` lifecycle function From 08bd9d644abbdebc40e63fa6c73909454e65fb1b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 11 Apr 2025 21:49:53 -0400 Subject: [PATCH 5/5] tweak --- packages/svelte/src/index-client.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 92e35ac558b5..816e2b0a113d 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -208,15 +208,15 @@ export function afterUpdate(fn) { * @returns {void} */ export function onAnimationFrame(fn) { - onMount(() => { - let frame = -1; + if (component_context === null) { + lifecycle_outside_component('onAnimationFrame'); + } - function next() { + user_effect(() => { + let frame = requestAnimationFrame(function next() { frame = requestAnimationFrame(next); fn(); - } - - next(); + }); return () => { cancelAnimationFrame(frame);