Re-rendering occurs before the execution of the #if directive.
#16072
Development PRs
While looking into #16263 I realised that we can drastically simplify the prop implementation now that deriveds are writable. Some of the code in red is pretty hard to understand, but deleting it causes no tests to fail. Just to be safe, I added a changeset so that this creates a new version that we can roll back from if it causes any issues.
Before submitting the PR, please make sure you do the following
- It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
- Prefix your PR title with
feat:,fix:,chore:, ordocs:. - This message body should clearly illustrate what problems it solves.
- Ideally, include a test that fails without this PR but passes with it.
- If this PR changes code within
packages/svelte/src, add a changeset (npx changeset).
Tests and linting
- Run the tests with
pnpm testand lint the project withpnpm lint
alternative to #16263, made possible by #16270. Closes #16114, closes #16262
Before submitting the PR, please make sure you do the following
- It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
- Prefix your PR title with
feat:,fix:,chore:, ordocs:. - This message body should clearly illustrate what problems it solves.
- Ideally, include a test that fails without this PR but passes with it.
- If this PR changes code within
packages/svelte/src, add a changeset (npx changeset).
Tests and linting
- Run the tests with
pnpm testand lint the project withpnpm lint
Fixes #16072. It's a real edge case, hence the convoluted nature of the test, but it's possible for a state change in one effect to cause a subsequent already-scheduled effect to run even though the second effect would be destroyed if the effect tree was being processed from the root.
The solution, I think, is to abort the processing of effects if a state change occurs. We can schedule the remaining effects and start again from the top; this will ensure that any dirty branches (i.e. a now-falsy if containing an effect with a now-broken reference) are updated before their children. Within each flush of the effect tree, predictable order is preserved.
We need to distinguish between user effects and non-user effects, since element bindings can result in state changes, and these should not cause flushing to be aborted (they are guaranteed to run before the effects that depend on the changed state, so this is not a problem).
Before submitting the PR, please make sure you do the following
- It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
- Prefix your PR title with
feat:,fix:,chore:, ordocs:. - This message body should clearly illustrate what problems it solves.
- Ideally, include a test that fails without this PR but passes with it.
- If this PR changes code within
packages/svelte/src, add a changeset (npx changeset).
Tests and linting
- Run the tests with
pnpm testand lint the project withpnpm lint
Alternative to #16612 which I think is probably a bit simpler, and hopefully results in a greater performance boost.
Whereas #16612 works by flushing parts of the effect tree when an invalidation occurs, such that to-be-destroyed effects are destroyed before they can re-execute, this comes from the opposite end — we continue to abort flushing if a state change occurs in a user effect, but only if that state change results in an existing effect being marked (the cause of #16072).
Fixes #16548.
Before submitting the PR, please make sure you do the following
- It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
- Prefix your PR title with
feat:,fix:,chore:, ordocs:. - This message body should clearly illustrate what problems it solves.
- Ideally, include a test that fails without this PR but passes with it.
- If this PR changes code within
packages/svelte/src, add a changeset (npx changeset).
Tests and linting
- Run the tests with
pnpm testand lint the project withpnpm lint
Another stab at #16548. The idea is that if a block effect is dirtied by a state change in another effect, it jumps to the head of the queue rather than waiting for the next root-to-tip traversal. That way, if it ends up destroying its children, they get destroyed immediately, rather than running even though they're about to be destroyed (which can result in errors — #16072).
Because we're not causing extra traversals, this (as far as I've been able to determine) fully removes the performance overhead introduced by #16280, without regressing on correctness. It also fixes the false positive infinite loop detection.
The one part I really don't love is the old_values.clear() — it gets the tests passing (one fails without it) but feels wrong. I stole it from #16612 — maybe @dummdidumm can explain if/why/how it's okay.
It also seems weird that we need to schedule the block effects in addition to running them eagerly, but if we don't then one test fails. Will investigate when my brain isn't running on fumes.
Before submitting the PR, please make sure you do the following
- It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
- Prefix your PR title with
feat:,fix:,chore:, ordocs:. - This message body should clearly illustrate what problems it solves.
- Ideally, include a test that fails without this PR but passes with it.
- If this PR changes code within
packages/svelte/src, add a changeset (npx changeset).
Tests and linting
- Run the tests with
pnpm testand lint the project withpnpm lint
Issue
Describe the bug
I have a Modal Component that provides the style of Modal, content slot, and methods to open and close Modal.
The control method of opening and closing is implemented through the state of current,and current is set to undefiend when closing.
When calling this component, if there are child components within chilren, and first-level child component has an $effect, and second-level child component calls the clse methods, an exception whill be thrown.
Uncaught TypeError: Cannot read properties of undefined (reading 'model')
I have a #if check for the data usage, but the error still occurs. I suspect that the child components'rerendering executes earlier than the #if check.
test.svelte.ts(data model):
type State = 'init' | 'submitted';
export function createModal() {
let state = $state<State>('init');
return {
get state() {
return state;
},
submit() {
state = 'submitted';
}
};
}
export type Model = ReturnType<typeof createModal>;
Modal.svelte:
<script lang="ts" module>
let current = $state<{ model: Model }>();
export function open(model: Model) {
current = {
model
};
}
export function close() {
current = undefined;
}
</script>
<script lang="ts">
import { onDestroy, type Snippet } from 'svelte';
import type { Model } from './test.svelte';
let {
children
}: {
children: Snippet<[Model]>;
} = $props();
onDestroy(() => {
console.log('destroy test');
});
</script>
{#if current?.model}
{@render children(current.model)}
{/if}
+page.svelte
<script>
import { createModal } from './test.svelte';
import TestChildren1 from './TestChildren1.svelte';
import TestComponent, { open } from './Modal.svelte';
const model = createModal();
$effect(() => {
console.log('model', model);
});
</script>
<button
onclick={() => {
open(model);
}}>open</button
>
<TestComponent>
{#snippet children(model)}
<TestChildren1 {model} />
{/snippet}
</TestComponent>
TestChildren1:
<script lang="ts">
import { onDestroy } from 'svelte';
import TestChildren2 from './TestChildren2.svelte';
import type { Model } from './test.svelte';
let {
model
}: {
model: Model;
} = $props();
$effect(() => {
console.log('b', JSON.stringify(model));
});
onDestroy(() => {
console.log('destroy component 1');
});
</script>
<span>{model.state}</span>
<TestChildren2 {model} />
TestChildren2:
<script lang="ts">
import { onDestroy } from 'svelte';
import { close } from './Modal.svelte';
import type { Model } from './test.svelte';
let {
model
}: {
model: Model;
} = $props();
$effect(() => {
if (model.state === 'submitted') close();
});
setTimeout(() => {
model.submit();
}, 1000);
onDestroy(() => {
console.log('destroy component 2');
});
</script>
<span>{model.state}</span>
Reproduction
example:https://github.com/Sincenir/svelte-issues-effect-and-modal
localhost... /test2
Logs
System Info
chunk-ZPWZ3TV6.js?v=a4869f9a:1549 Uncaught TypeError: Cannot read properties of undefined (reading 'model')
in $effect
in TestChildren1.svelte
in Modal.svelte
in +page.svelte
in +layout.svelte
in root.svelte
at Modal.svelte:29:27
at get model (+page.svelte:19:26)
at $effect (TestChildren1.svelte:13:34)
(匿名) @ Modal.svelte:29
get model @ +page.svelte:19
$effect @ TestChildren1.svelte:13
setTimeout
TestChildren2 @ TestChildren2.svelte:16
TestChildren1 @ TestChildren1.svelte:18
(匿名) @ +page.svelte:19
consequent @ Modal.svelte:25
(匿名) @ Modal.svelte:28
Severity
blocking an upgrade
Info
It seems that the effect inside TestChildren2.svelte does kick off the chain that leads to the error. It closes the modal, and then the render tag inside Modal.svelte is executed even though it shouldn't be because the surrounding if should've already made it falsy.
It seems that the effect inside
TestChildren2.sveltedoes kick off the chain that leads to the error. It closes the modal, and then the render tag insideModal.svelteis executed even though it shouldn't be because the surrounding if should've already made it falsy.
This problem only occurs when TestChildren2 uses $effect. The same applies when using $inspect.
Seems like we are also experiencing this issue a lot in production
I was exploring a bit earlier and at least from my exploration the real problem is that the $effect in TestChildren1 is executed when it shouldn't...if you remove that it's not problematic anymore. Will try to check what is happening in a bit.
This is what happens:
model.submit()is called, which setsmodel.stateto"submitted"- all effects depending on
modelare scheduled. That's the$effects insideTestChildren2.svelteandTestChildren1.svelte.$effects run child before parent. - the
$effectinsideTestChildren2.sveltetherefore runs first, callingclose()which sets the whole model toundefined - then the
$effectinsideTestChildren1.svelteruns, which usesmodel. modelunder the hood - since it's a prop - is modeled as a getter. The getter is insideApp.sveltebecause that's whereTestChildren1.svelteis used. The getter just forwards its call, i.e. it invoked the passed snippet parameter. Said snippet parameter comes fromModal.svelteand iscurrent.model- Since
currentis now undefined,current.modelfails with the runtime error
-> ultimately this is another incarnation of props updating their values in unexpected ways (#14725) and #15469 didn't solve that problem since its logic only kicks in for teardown functions
This is what happens:
model.submit()is called, which setsmodel.stateto"submitted"- all effects depending on
modelare scheduled. That's the$effects insideTestChildren2.svelteandTestChildren1.svelte.$effects run child before parent.- the
$effectinsideTestChildren2.sveltetherefore runs first, callingclose()which sets the whole model toundefined- then the
$effectinsideTestChildren1.svelteruns, which usesmodel.modelunder the hood - since it's a prop - is modeled as a getter. The getter is insideApp.sveltebecause that's whereTestChildren1.svelteis used. The getter just forwards its call, i.e. it invoked the passed snippet parameter. Said snippet parameter comes fromModal.svelteand iscurrent.model- Since
currentis now undefined,current.modelfails with the runtime error-> ultimately this is another incarnation of props updating their values in unexpected ways (#14725) and #15469 didn't solve that problem since its logic only kicks in for teardown functions
I think there's one point here that is missing: the effect depending on model should not be scheduled because model it's not really changing (model.state does)...however the problem is the process_effects collect ALL the effects and only run the dirty ones...and after the effect in TestChildren2.svelte runs the effect that was collected as CLEAN is now DIRTY and so it runs.
It's doing JSON.stringify(model) so it does depend on model.state. But you're right that without it the effect shouldn't run in the same scheduled task if it's only doing model.
It's doing
JSON.stringify(model)so it does depend onmodel.state. But you're right that without it the effect shouldn't run in the same scheduled task if it's only doingmodel.
Oh lol i got so used to my repro where i removed the JSON.stringify i forgot...however you just need to get model in it for it to fail which is even weirder.
Btw this is once again why you really shouldn't assign state in effects.
After looking at this with Paolo we see two ways out of this:
- a) create shallow copies of props. This way when a getter would already return a new value it will still return the old value, instead the value would be updated in a render effect, so that they update together with other parts in the template. In other words you would think about components like functions and props like function arguments - they are also shallow copies (i.e. if you reassign a value in a parent component it won't be reflected in the child). Drawback is mostly decreased performance (though we could potentially mitigate that through static analysis to see when it's actually necessary).
- b) adjust
flush_queued_effectssuch that we somehow check after every loop if any branch effects should now run, e.g. in this example we would see "oh now the if branch is dirty we need to run it first before processing others", and that would stop the other effects from running. Drawback is that this may mess with people's expectations around effect processing order (like, you could argue the parent effect should still run with the old value before the if block tears it down)
Neither is ideal so I'm open to secret option c) but I don't see one right now.
After testing, this issue was not fixed with version 5.35.4. @dummdidumm @paoloricciuti @Rich-Harris
This issue should really be reopened, given still exhibits the bad behavior.
The link points to an old Svelte version, with latest I don't see a type error anymore. If there's another way to get this bug we need another reproduction
@dummdidumm it still happens for me if I press open a bunch of times in a row
Yep, same for me
In the REPL specifically it still occurs because of the setTimeout running regardless of whether or not the component was already destroyed (because on second click it will open then destroy again right away). Is there another reproduction where it does occur without something like that?