Skip to content

Commit 623340a

Browse files
authored
fix: make beforeUpdate/afterUpdate behave more like Svelte 4 (#10408)
Closes #9420. This PR creates an $effect.pre (before beforeUpdate and an $effect (for afterUpdate) and, inside those, listen for all locally declared signals plus reactive props. This does mean that we need to link the locally declared signals to the component context (the reverse of the current behaviour, wherein we link the component context to locally declared signals).
1 parent 983db98 commit 623340a

File tree

23 files changed

+291
-156
lines changed

23 files changed

+291
-156
lines changed

.changeset/afraid-dogs-matter.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: align `beforeUpdate`/`afterUpdate` behavior better with that in Svelte 4

packages/svelte/src/compiler/errors.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,8 @@ const runes = {
200200
`${rune} can only be called with ${list(args, 'or')} ${
201201
args.length === 1 && args[0] === 1 ? 'argument' : 'arguments'
202202
}`,
203+
/** @param {string} name */
204+
'invalid-runes-mode-import': (name) => `${name} cannot be used in runes mode`,
203205
'duplicate-props-rune': () => `Cannot use $props() more than once`
204206
};
205207

packages/svelte/src/compiler/phases/2-analyze/validation.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,20 @@ const validation = {
492492
error(node, 'invalid-const-placement');
493493
}
494494
},
495+
ImportDeclaration(node, context) {
496+
if (node.source.value === 'svelte' && context.state.analysis.runes) {
497+
for (const specifier of node.specifiers) {
498+
if (specifier.type === 'ImportSpecifier') {
499+
if (
500+
specifier.imported.name === 'beforeUpdate' ||
501+
specifier.imported.name === 'afterUpdate'
502+
) {
503+
error(specifier, 'invalid-runes-mode-import', specifier.imported.name);
504+
}
505+
}
506+
}
507+
}
508+
},
495509
LetDirective(node, context) {
496510
const parent = context.path.at(-1);
497511
if (

packages/svelte/src/compiler/phases/3-transform/client/transform-client.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ export function client_component(source, analysis, options) {
265265
...legacy_reactive_declarations,
266266
...group_binding_declarations,
267267
.../** @type {import('estree').Statement[]} */ (instance.body),
268+
analysis.runes ? b.empty : b.stmt(b.call('$.init')),
268269
.../** @type {import('estree').Statement[]} */ (template.body),
269270
...static_bindings
270271
]);

packages/svelte/src/internal/client/runtime.js

Lines changed: 79 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { DEV } from 'esm-env';
22
import { subscribe_to_store } from '../../store/utils.js';
3-
import { noop, run_all } from '../common.js';
3+
import { noop, run, run_all } from '../common.js';
44
import {
55
array_prototype,
66
get_descriptor,
@@ -104,18 +104,9 @@ export let current_block = null;
104104

105105
/** @type {import('./types.js').ComponentContext | null} */
106106
export let current_component_context = null;
107-
export let is_ssr = false;
108107

109108
export let updating_derived = false;
110109

111-
/**
112-
* @param {boolean} ssr
113-
* @returns {void}
114-
*/
115-
export function set_is_ssr(ssr) {
116-
is_ssr = ssr;
117-
}
118-
119110
/**
120111
* @param {null | import('./types.js').ComponentContext} context
121112
* @returns {boolean}
@@ -147,14 +138,6 @@ export function batch_inspect(target, prop, receiver) {
147138
};
148139
}
149140

150-
/**
151-
* @param {null | import('./types.js').ComponentContext} context_stack_item
152-
* @returns {void}
153-
*/
154-
export function set_current_component_context(context_stack_item) {
155-
current_component_context = context_stack_item;
156-
}
157-
158141
/**
159142
* @param {unknown} a
160143
* @param {unknown} b
@@ -181,8 +164,6 @@ function create_source_signal(flags, value) {
181164
f: flags,
182165
// value
183166
v: value,
184-
// context: We can remove this if we get rid of beforeUpdate/afterUpdate
185-
x: null,
186167
// this is for DEV only
187168
inspect: new Set()
188169
};
@@ -195,9 +176,7 @@ function create_source_signal(flags, value) {
195176
// flags
196177
f: flags,
197178
// value
198-
v: value,
199-
// context: We can remove this if we get rid of beforeUpdate/afterUpdate
200-
x: null
179+
v: value
201180
};
202181
}
203182

@@ -346,12 +325,6 @@ function execute_signal_fn(signal) {
346325
current_skip_consumer = !is_flushing_effect && (flags & UNOWNED) !== 0;
347326
current_untracking = false;
348327

349-
// Render effects are invoked when the UI is about to be updated - run beforeUpdate at that point
350-
if (is_render_effect && current_component_context?.u != null) {
351-
// update_callbacks.execute()
352-
current_component_context.u.e();
353-
}
354-
355328
try {
356329
let res;
357330
if (is_render_effect) {
@@ -924,7 +897,7 @@ export function store_set(store, value) {
924897
* @param {import('./types.js').StoreReferencesContainer} stores
925898
*/
926899
export function unsubscribe_on_destroy(stores) {
927-
onDestroy(() => {
900+
on_destroy(() => {
928901
let store_name;
929902
for (store_name in stores) {
930903
const ref = stores[store_name];
@@ -1150,7 +1123,7 @@ export function mark_subtree_inert(signal, inert, visited_blocks = new Set()) {
11501123
* @returns {void}
11511124
*/
11521125
function mark_signal_consumers(signal, to_status, force_schedule) {
1153-
const runes = is_runes(signal.x);
1126+
const runes = is_runes(null);
11541127
const consumers = signal.c;
11551128
if (consumers !== null) {
11561129
const length = consumers.length;
@@ -1192,7 +1165,7 @@ export function set_signal_value(signal, value) {
11921165
!current_untracking &&
11931166
!ignore_mutation_validation &&
11941167
current_consumer !== null &&
1195-
is_runes(signal.x) &&
1168+
is_runes(null) &&
11961169
(current_consumer.f & DERIVED) !== 0
11971170
) {
11981171
throw new Error(
@@ -1208,7 +1181,6 @@ export function set_signal_value(signal, value) {
12081181
(signal.f & SOURCE) !== 0 &&
12091182
!(/** @type {import('./types.js').EqualsFunctions} */ (signal.e)(value, signal.v))
12101183
) {
1211-
const component_context = signal.x;
12121184
signal.v = value;
12131185
// If the current signal is running for the first time, it won't have any
12141186
// consumers as we only allocate and assign the consumers after the signal
@@ -1219,7 +1191,7 @@ export function set_signal_value(signal, value) {
12191191
// $effect(() => x++)
12201192
//
12211193
if (
1222-
is_runes(component_context) &&
1194+
is_runes(null) &&
12231195
current_effect !== null &&
12241196
current_effect.c === null &&
12251197
(current_effect.f & CLEAN) !== 0
@@ -1236,19 +1208,6 @@ export function set_signal_value(signal, value) {
12361208
}
12371209
}
12381210
mark_signal_consumers(signal, DIRTY, true);
1239-
// If we have afterUpdates locally on the component, but we're within a render effect
1240-
// then we will need to manually invoke the beforeUpdate/afterUpdate logic.
1241-
// TODO: should we put this being a is_runes check and only run it in non-runes mode?
1242-
if (current_effect === null && current_queued_pre_and_render_effects.length === 0) {
1243-
const update_callbacks = component_context?.u;
1244-
if (update_callbacks != null) {
1245-
run_all(update_callbacks.b);
1246-
const managed = managed_effect(() => {
1247-
destroy_signal(managed);
1248-
run_all(update_callbacks.a);
1249-
});
1250-
}
1251-
}
12521211

12531212
// @ts-expect-error
12541213
if (DEV && signal.inspect) {
@@ -1299,7 +1258,7 @@ export function derived(init) {
12991258
create_computation_signal(flags | CLEAN, UNINITIALIZED, current_block)
13001259
);
13011260
signal.i = init;
1302-
signal.x = current_component_context;
1261+
bind_signal_to_component_context(signal);
13031262
signal.e = default_equals;
13041263
if (current_consumer !== null) {
13051264
push_reference(current_consumer, signal);
@@ -1327,10 +1286,27 @@ export function derived_safe_equal(init) {
13271286
/*#__NO_SIDE_EFFECTS__*/
13281287
export function source(initial_value) {
13291288
const source = create_source_signal(SOURCE | CLEAN, initial_value);
1330-
source.x = current_component_context;
1289+
bind_signal_to_component_context(source);
13311290
return source;
13321291
}
13331292

1293+
/**
1294+
* This function binds a signal to the component context, so that we can fire
1295+
* beforeUpdate/afterUpdate callbacks at the correct time etc
1296+
* @param {import('./types.js').Signal} signal
1297+
*/
1298+
function bind_signal_to_component_context(signal) {
1299+
if (current_component_context === null || !current_component_context.r) return;
1300+
1301+
const signals = current_component_context.d;
1302+
1303+
if (signals) {
1304+
signals.push(signal);
1305+
} else {
1306+
current_component_context.d = [signal];
1307+
}
1308+
}
1309+
13341310
/**
13351311
* @template V
13361312
* @param {V} initial_value
@@ -1883,18 +1859,11 @@ export function value_or_fallback(value, fallback) {
18831859

18841860
/**
18851861
* Schedules a callback to run immediately before the component is unmounted.
1886-
*
1887-
* Out of `onMount`, `beforeUpdate`, `afterUpdate` and `onDestroy`, this is the
1888-
* only one that runs inside a server-side component.
1889-
*
1890-
* https://svelte.dev/docs/svelte#ondestroy
18911862
* @param {() => any} fn
18921863
* @returns {void}
18931864
*/
1894-
export function onDestroy(fn) {
1895-
if (!is_ssr) {
1896-
user_effect(() => () => untrack(fn));
1897-
}
1865+
function on_destroy(fn) {
1866+
user_effect(() => () => untrack(fn));
18981867
}
18991868

19001869
/**
@@ -1914,6 +1883,8 @@ export function push(props, runes = false) {
19141883
m: false,
19151884
// parent
19161885
p: current_component_context,
1886+
// signals
1887+
d: null,
19171888
// props
19181889
s: props,
19191890
// runes
@@ -1945,6 +1916,56 @@ export function pop(accessors) {
19451916
}
19461917
}
19471918

1919+
/**
1920+
* Invoke the getter of all signals associated with a component
1921+
* so they can be registered to the effect this function is called in.
1922+
* @param {import('./types.js').ComponentContext} context
1923+
*/
1924+
function observe_all(context) {
1925+
if (context.d) {
1926+
for (const signal of context.d) get(signal);
1927+
}
1928+
1929+
const props = get_descriptors(context.s);
1930+
for (const descriptor of Object.values(props)) {
1931+
if (descriptor.get) descriptor.get();
1932+
}
1933+
}
1934+
1935+
/**
1936+
* Legacy-mode only: Call `onMount` callbacks and set up `beforeUpdate`/`afterUpdate` effects
1937+
*/
1938+
export function init() {
1939+
const context = /** @type {import('./types.js').ComponentContext} */ (current_component_context);
1940+
const callbacks = context.u;
1941+
1942+
if (!callbacks) return;
1943+
1944+
// beforeUpdate
1945+
pre_effect(() => {
1946+
observe_all(context);
1947+
callbacks.b.forEach(run);
1948+
});
1949+
1950+
// onMount (must run before afterUpdate)
1951+
user_effect(() => {
1952+
const fns = untrack(() => callbacks.m.map(run));
1953+
return () => {
1954+
for (const fn of fns) {
1955+
if (typeof fn === 'function') {
1956+
fn();
1957+
}
1958+
}
1959+
};
1960+
});
1961+
1962+
// afterUpdate
1963+
user_effect(() => {
1964+
observe_all(context);
1965+
callbacks.a.forEach(run);
1966+
});
1967+
}
1968+
19481969
/**
19491970
* @param {any} value
19501971
* @param {Set<any>} visited

packages/svelte/src/internal/client/types.d.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export type Store<V> = {
3636
// when the JS VM JITs the code.
3737

3838
export type ComponentContext = {
39+
/** local signals (needed for beforeUpdate/afterUpdate) */
40+
d: null | Signal<any>[];
3941
/** props */
4042
s: Record<string, unknown>;
4143
/** accessors */
@@ -52,12 +54,12 @@ export type ComponentContext = {
5254
r: boolean;
5355
/** update_callbacks */
5456
u: null | {
55-
/** before */
56-
b: Array<() => void>;
57-
/** after */
57+
/** afterUpdate callbacks */
5858
a: Array<() => void>;
59-
/** execute */
60-
e: () => void;
59+
/** beforeUpdate callbacks */
60+
b: Array<() => void>;
61+
/** onMount callbacks */
62+
m: Array<() => any>;
6163
};
6264
};
6365

@@ -69,8 +71,6 @@ export type ComponentContext = {
6971
export type SourceSignal<V = unknown> = {
7072
/** consumers: Signals that read from the current signal */
7173
c: null | ComputationSignal[];
72-
/** context: The associated component if this signal is an effect/computed */
73-
x: null | ComponentContext;
7474
/** equals: For value equality */
7575
e: null | EqualsFunctions;
7676
/** flags: The types that the signal represent, as a bitwise value */

packages/svelte/src/internal/common.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,8 @@ export function run_all(arr) {
1919
arr[i]();
2020
}
2121
}
22+
23+
/** @param {Function} fn */
24+
export function run(fn) {
25+
return fn();
26+
}

packages/svelte/src/internal/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,15 @@ export {
3030
exclude_from_object,
3131
store_set,
3232
unsubscribe_on_destroy,
33-
onDestroy,
3433
pop,
3534
push,
3635
reactive_import,
3736
effect_active,
3837
user_root_effect,
3938
inspect,
4039
unwrap,
41-
freeze
40+
freeze,
41+
init
4242
} from './client/runtime.js';
4343
export * from './client/each.js';
4444
export * from './client/render.js';

packages/svelte/src/internal/server/index.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as $ from '../client/runtime.js';
2-
import { set_is_ssr } from '../client/runtime.js';
32
import { is_promise, noop } from '../common.js';
43
import { subscribe_to_store } from '../../store/utils.js';
54
import { DOMBooleanAttributes } from '../../constants.js';
@@ -95,7 +94,6 @@ export function render(component, options) {
9594
const root_anchor = create_anchor(payload);
9695
const root_head_anchor = create_anchor(payload.head);
9796

98-
set_is_ssr(true);
9997
const prev_on_destroy = on_destroy;
10098
on_destroy = [];
10199
payload.out += root_anchor;
@@ -112,7 +110,6 @@ export function render(component, options) {
112110
payload.out += root_anchor;
113111
for (const cleanup of on_destroy) cleanup();
114112
on_destroy = prev_on_destroy;
115-
set_is_ssr(false);
116113

117114
return {
118115
head:

0 commit comments

Comments
 (0)