diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index da24084d4dd0..e346bceba81c 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -2,15 +2,11 @@ title: $effect --- -Effects are what make your application _do things_. When Svelte runs an effect function, it tracks which pieces of state (and derived state) are accessed (unless accessed inside [`untrack`](svelte#untrack)), and re-runs the function when that state later changes. +Effects are functions that run when state updates, and can be used for things like calling third-party libraries, drawing on `` elements, or making network requests. They only run in the browser, not during server-side rendering. -Most of the effects in a Svelte app are created by Svelte itself — they're the bits that update the text in `

hello {name}!

` when `name` changes, for example. +Generally speaking, you should _not_ update state inside effects, as it will make code more convoluted and will often lead to never-ending update cycles. If you find yourself doing so, see [when not to use `$effect`](#When-not-to-use-$effect) to learn about alternative approaches. -But you can also create your own effects with the `$effect` rune, which is useful when you need to synchronize an external system (whether that's a library, or a `` element, or something across a network) with state inside your Svelte app. - -> [!NOTE] Avoid overusing `$effect`! When you do too much work in effects, code often becomes difficult to understand and maintain. See [when not to use `$effect`](#When-not-to-use-$effect) to learn about alternative approaches. - -Your effects run after the component has been mounted to the DOM, and in a [microtask](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide) after state changes ([demo](/playground/untitled#H4sIAAAAAAAAE31S246bMBD9lZF3pSRSAqTVvrCAVPUP2sdSKY4ZwJJjkD0hSVH-vbINuWxXfQH5zMyZc2ZmZLVUaFn6a2R06ZGlHmBrpvnBvb71fWQHVOSwPbf4GS46TajJspRlVhjZU1HqkhQSWPkHIYdXS5xw-Zas3ueI6FRn7qHFS11_xSRZhIxbFtcDtw7SJb1iXaOg5XIFeQGjzyPRaevYNOGZIJ8qogbpe8CWiy_VzEpTXiQUcvPDkSVrSNZz1UlW1N5eLcqmpdXUvaQ4BmqlhZNUCgxuzFHDqUWNAxrYeUM76AzsnOsdiJbrBp_71lKpn3RRbii-4P3f-IMsRxS-wcDV_bL4PmSdBa2wl7pKnbp8DMgVvJm8ZNskKRkEM_OzyOKQFkgqOYBQ3Nq89Ns0nbIl81vMFN-jKoLMTOr-SOBOJS-Z8f5Y6D1wdcR8dFqvEBdetK-PHwj-z-cH8oHPY54wRJ8Ys7iSQ3Bg3VA9azQbmC9k35kKzYa6PoVtfwbbKVnBixBiGn7Pq0rqJoUtHiCZwAM3jdTPWCVtr_glhVrhecIa3vuksJ_b7TqFs4DPyriSjd5IwoNNQaAmNI-ESfR2p8zimzvN1swdCkvJHPH6-_oX8o1SgcIDAAA=)): +You can create an effect with the `$effect` rune ([demo](/playground/untitled#H4sIAAAAAAAAE31S246bMBD9lZF3pSRSAqTVvrCAVPUP2sdSKY4ZwJJjkD0hSVH-vbINuWxXfQH5zMyZc2ZmZLVUaFn6a2R06ZGlHmBrpvnBvb71fWQHVOSwPbf4GS46TajJspRlVhjZU1HqkhQSWPkHIYdXS5xw-Zas3ueI6FRn7qHFS11_xSRZhIxbFtcDtw7SJb1iXaOg5XIFeQGjzyPRaevYNOGZIJ8qogbpe8CWiy_VzEpTXiQUcvPDkSVrSNZz1UlW1N5eLcqmpdXUvaQ4BmqlhZNUCgxuzFHDqUWNAxrYeUM76AzsnOsdiJbrBp_71lKpn3RRbii-4P3f-IMsRxS-wcDV_bL4PmSdBa2wl7pKnbp8DMgVvJm8ZNskKRkEM_OzyOKQFkgqOYBQ3Nq89Ns0nbIl81vMFN-jKoLMTOr-SOBOJS-Z8f5Y6D1wdcR8dFqvEBdetK-PHwj-z-cH8oHPY54wRJ8Ys7iSQ3Bg3VA9azQbmC9k35kKzYa6PoVtfwbbKVnBixBiGn7Pq0rqJoUtHiCZwAM3jdTPWCVtr_glhVrhecIa3vuksJ_b7TqFs4DPyriSjd5IwoNNQaAmNI-ESfR2p8zimzvN1swdCkvJHPH6-_oX8o1SgcIDAAA=)): ```svelte ``` +Note that [when `$effect` runs is different]($effect#Understanding-dependencies) than when `$:` runs. + > [!DETAILS] Why we did this > `$:` was a great shorthand and easy to get started with: you could slap a `$:` in front of most code and it would somehow work. This intuitiveness was also its drawback the more complicated your code became, because it wasn't as easy to reason about. Was the intent of the code to create a derivation, or a side effect? With `$derived` and `$effect`, you have a bit more up-front decision making to do (spoiler alert: 90% of the time you want `$derived`), but future-you and other developers on your team will have an easier time. > diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 44f1f7cb7972..a05938dacce3 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.22.6 + +### Patch Changes + +- fix: skip `log_if_contains_state` if only logging literals ([#15468](https://github.com/sveltejs/svelte/pull/15468)) + +- fix: Add `closedby` property to HTMLDialogAttributes type ([#15458](https://github.com/sveltejs/svelte/pull/15458)) + +- fix: null and warnings for local handlers ([#15460](https://github.com/sveltejs/svelte/pull/15460)) + ## 5.22.5 ### Patch Changes diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 6d256b56205c..08687cafaf14 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -957,6 +957,7 @@ export interface HTMLDelAttributes extends HTMLAttributes { export interface HTMLDialogAttributes extends HTMLAttributes { open?: boolean | undefined | null; + closedby?: 'any' | 'closerequest' | 'none' | undefined | null; } export interface HTMLEmbedAttributes extends HTMLAttributes { diff --git a/packages/svelte/package.json b/packages/svelte/package.json index fb20167a4e73..53f03e3543fc 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.22.5", + "version": "5.22.6", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js index 7a3057451aa1..fda43ad7911a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js @@ -44,7 +44,8 @@ export function CallExpression(node, context) { node.callee.property.type === 'Identifier' && ['debug', 'dir', 'error', 'group', 'groupCollapsed', 'info', 'log', 'trace', 'warn'].includes( node.callee.property.name - ) + ) && + node.arguments.some((arg) => arg.type !== 'Literal') // TODO more cases? ) { return b.call( node.callee, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js index f23f7548ece1..2667a96f6aef 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js @@ -46,8 +46,12 @@ export function visit_event_attribute(node, context) { // When we hoist a function we assign an array with the function and all // hoisted closure params. - const args = [handler, ...hoisted_params]; - delegated_assignment = b.array(args); + if (hoisted_params) { + const args = [handler, ...hoisted_params]; + delegated_assignment = b.array(args); + } else { + delegated_assignment = handler; + } } else { delegated_assignment = handler; } @@ -123,11 +127,19 @@ export function build_event_handler(node, metadata, context) { } // function declared in the script - if ( - handler.type === 'Identifier' && - context.state.scope.get(handler.name)?.declaration_kind !== 'import' - ) { - return handler; + if (handler.type === 'Identifier') { + const binding = context.state.scope.get(handler.name); + + if (binding?.is_function()) { + return handler; + } + + // local variable can be assigned directly + // except in dev mode where when need $.apply() + // in order to handle warnings. + if (!dev && binding?.declaration_kind !== 'import') { + return handler; + } } if (metadata.has_call) { diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 7d9f90982afb..a5227c1b5113 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -79,6 +79,21 @@ export class Binding { get updated() { return this.mutated || this.reassigned; } + + is_function() { + if (this.reassigned) { + // even if it's reassigned to another function, + // we can't use it directly as e.g. an event handler + return false; + } + + if (this.declaration_kind === 'function') { + return true; + } + + const type = this.initial?.type; + return type === 'ArrowFunctionExpression' || type === 'FunctionExpression'; + } } export class Scope { diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index efcf7b727b8d..fd8e999da763 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -45,13 +45,14 @@ if (DEV) { } /** - * The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM. - * It must be called during the component's initialisation (but doesn't need to live *inside* the component; - * it can be called from an external module). + * `onMount`, like [`$effect`](https://svelte.dev/docs/svelte/$effect), schedules a function to run as soon as the component has been mounted to the DOM. + * Unlike `$effect`, the provided function only runs once. * - * If a function is returned _synchronously_ from `onMount`, it will be called when the component is unmounted. + * It must be called during the component's initialisation (but doesn't need to live _inside_ the component; + * it can be called from an external module). If a function is returned _synchronously_ from `onMount`, + * it will be called when the component is unmounted. * - * `onMount` does not run inside [server-side components](https://svelte.dev/docs/svelte/svelte-server#render). + * `onMount` functions do not run during [server-side rendering](https://svelte.dev/docs/svelte/svelte-server#render). * * @template T * @param {() => NotFunction | Promise> | (() => any)} fn diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index 25ece5f569d7..0c1bb1dada83 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -238,7 +238,7 @@ export function handle_event_propagation(event) { var delegated = current_target['__' + event_name]; if ( - delegated !== undefined && + delegated != null && (!(/** @type {any} */ (current_target).disabled) || // DOM could've been updated already by the time this is reached, so we check this as well // -> the target could not have been disabled because it emits the event in the first place @@ -311,13 +311,11 @@ export function apply( error = e; } - if (typeof handler === 'function') { - handler.apply(element, args); - } else if (has_side_effects || handler != null || error) { + if (typeof handler !== 'function' && (has_side_effects || handler != null || error)) { const filename = component?.[FILENAME]; const location = loc ? ` at ${filename}:${loc[0]}:${loc[1]}` : ` in ${filename}`; - - const event_name = args[0].type; + const phase = args[0]?.eventPhase < Event.BUBBLING_PHASE ? 'capture' : ''; + const event_name = args[0]?.type + phase; const description = `\`${event_name}\` handler${location}`; const suggestion = remove_parens ? 'remove the trailing `()`' : 'add a leading `() =>`'; @@ -327,4 +325,5 @@ export function apply( throw error; } } + handler?.apply(element, args); } diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index e20a9683ddc6..01e1b390a39e 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.22.5'; +export const VERSION = '5.22.6'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/_config.js b/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/_config.js new file mode 100644 index 000000000000..d53812d4c39e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/_config.js @@ -0,0 +1,48 @@ +import { assertType } from 'vitest'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + + compileOptions: { + dev: true + }, + + test({ assert, target, warnings, logs }) { + /** @type {any} */ + let error = null; + + const handler = (/** @type {any} */ e) => { + error = e.error; + e.stopImmediatePropagation(); + }; + + window.addEventListener('error', handler, true); + + const [b1, b2, b3] = target.querySelectorAll('button'); + + b1.click(); + assert.deepEqual(logs, []); + assert.equal(error, null); + + error = null; + logs.length = 0; + + b2.click(); + assert.deepEqual(logs, ['clicked']); + assert.equal(error, null); + + error = null; + logs.length = 0; + + b3.click(); + assert.deepEqual(logs, []); + assert.deepEqual(warnings, [ + '`click` handler at main.svelte:10:17 should be a function. Did you mean to add a leading `() =>`?' + ]); + assert.isNotNull(error); + assert.match(error.message, /is not a function/); + + window.removeEventListener('error', handler, true); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/main.svelte new file mode 100644 index 000000000000..f6e344ece8cf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/main.svelte @@ -0,0 +1,10 @@ + + + + + diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index c3dbdcac791e..c6000fc4b67f 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -349,13 +349,14 @@ declare module 'svelte' { props: Props; }); /** - * The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM. - * It must be called during the component's initialisation (but doesn't need to live *inside* the component; - * it can be called from an external module). + * `onMount`, like [`$effect`](https://svelte.dev/docs/svelte/$effect), schedules a function to run as soon as the component has been mounted to the DOM. + * Unlike `$effect`, the provided function only runs once. * - * If a function is returned _synchronously_ from `onMount`, it will be called when the component is unmounted. + * It must be called during the component's initialisation (but doesn't need to live _inside_ the component; + * it can be called from an external module). If a function is returned _synchronously_ from `onMount`, + * it will be called when the component is unmounted. * - * `onMount` does not run inside [server-side components](https://svelte.dev/docs/svelte/svelte-server#render). + * `onMount` functions do not run during [server-side rendering](https://svelte.dev/docs/svelte/svelte-server#render). * * */ export function onMount(fn: () => NotFunction | Promise> | (() => any)): void; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy