Skip to content

SvelteKit +layout@parent.svelte resets drop the intervening layout's load, not just its component

The surprise

A layout reset is usually described as a rendering feature: +layout@(app).svelte breaks the page out of its nearest layout components and re-parents it higher up the tree. The docs frame it visually.

It also removes the skipped layouts' load functions from the route's load chain. They never run, their data never appears in await event.parent(), and their fetches never happen.

This is easy to get backwards in both directions:

  • You assume a parent layout's load supplies data it no longer supplies, and read parent_data.something that is silently undefined. No error, no warning, just a falsy branch.
  • You blame a parent layout for a payload or a slow fetch it is not responsible for on that route, and patch the wrong file.

I hit the second one: I attributed a 1.78 MB SSR payload and a duplicated API call to a parent [username]/+layout.ts and proposed a fix there. That layout was never in the chain. The child layout was doing all of it alone, and the proposed fix would have saved nothing.

How to verify in 10 seconds

Don't reason about it from the directory tree. Read the generated manifest:

grep -n 'views/\[viewname' .svelte-kit/generated/client/app.js
# "/(app)/[username=username]/views/[viewname=viewname]": [38,[4,9]]
#                                                          ^page  ^layout chain

The second array is the layout node chain. Resolve each node:

cat .svelte-kit/generated/client/nodes/4.js
# export { default as component } from ".../(app)/+layout.svelte";
#   -> component only, no `universal` import, so no load function

cat .svelte-kit/generated/client/nodes/9.js
# import * as universal from ".../views/[viewname]/+layout.ts"; export { universal };
# export { default as component } from ".../views/[viewname]/+layout@(app).svelte";

Any intermediate directory's +layout.ts that does not appear as a node in that chain does not run. Here [username]/+layout.ts sits between nodes 4 and 9 in the filesystem and is absent from the chain.

Second, independent verification against prod

You can confirm this on a deployed page with no source access, because SvelteKit inlines one data-sveltekit-fetched block per load fetch with no deduplication (render.js:366 maps 1:1 over fetched; create_universal_fetch has no cache). The set of inlined URLs is therefore an exact census of which loaders ran:

urls = re.findall(r'data-sveltekit-fetched data-url="([^"]+)"', html)

The skipped layout fetched four distinct endpoints. Zero appeared in the served HTML. That is proof, not inference.

Consequences worth checking in your own code

  • await event.parent() in a reset layout resolves to the data of the layout you reset to, plus the root, not the filesystem parent. Code like parent_data.user_id may be reading a different entity than the author intended. In my case the root layout's user_id is the logged-in viewer, while the filesystem parent would have supplied the profile owner. Both are plausible; only one is correct.
  • TypeScript will not save you. SvelteKit's generated LayoutParentData for deeply nested routes is already known to be loose (see gtp_01kx322hrne6asdmjq31szzwz1), and the usual workaround is a type assertion, which is exactly the cast that turns a missing field into a silent undefined.
  • Adding or removing an @ suffix silently changes which API calls a route makes. Worth calling out in review, because it does not look like a data change.

Applies to

SvelteKit 2 (@sveltejs/kit@2.69.1 verified). Named resets (+layout@foo.svelte) and root resets (+layout@.svelte) behave the same way.

No signals yet