The Shared Data Problem

In the previous article we established how a single page fetches its data through a load function in +page.ts. That model works well when a route stands alone. In practice, though, routes rarely stand alone. A blog has dozens of posts that all live under the same /blog section. A dashboard has dozens of views that all share a sidebar navigation. A SaaS application has dozens of pages that all need to know who the current user is.

Fetching that shared data inside every individual page load function would be wasteful and error-prone. Wasteful because the same query runs redundantly on every navigation, error-prone because any change to the shared data structure has to be replicated across every file that fetches it.

SvelteKit solves this with layout load functions. Just as +layout.svelte provides shared UI that wraps every page within a route segment, +layout.ts (or +layout.server.ts) provides shared data that is available to every page within that same segment. The two systems mirror each other by design, and once you understand one, the other follows naturally.

This article explains how page-level and layout-level data loading work together, how data flows through the layout hierarchy, how SvelteKit merges data from multiple sources, and how to access data in both directions: downward from parent to child, and upward from page to parent layout.


One Page, Many Sources

Consider a blog application. The route structure looks like this:

src/routes/
├── +layout.svelte          ← root layout, wraps everything
├── +layout.server.ts       ← root layout load (user session, nav data)
├── blog/
│   ├── +layout.svelte      ← blog section layout, sidebar with post list
│   ├── +layout.server.ts   ← blog layout load (list of all posts)
│   └── [slug]/
│       ├── +page.svelte    ← individual post page
│       └── +page.server.ts ← post page load (the specific post)

The individual post page needs one specific article, that is page-level data, unique to this route. The blog section needs a list of all posts for the sidebar, that is shared across every post under /blog, so it belongs in the blog layout load. The root layout needs the current user’s session for the navigation header, that is shared across the entire application, so it belongs in the root layout load.

Without layout load functions, you would have two bad options: fetch the post list in every individual post’s load function (redundant and inconsistent), or use some out-of-band mechanism like a store initialised at application startup (fragile and disconnected from SvelteKit’s data pipeline). Layout load functions give you a third option that is neither of those: declare the data once at the right level in the hierarchy and let SvelteKit distribute it.


Principles

SvelteKit’s layout system and its data loading system are parallel hierarchies that map onto the same route directory structure. For every +layout.svelte file there can be a +layout.ts or +layout.server.ts alongside it, and they behave exactly like their +page counterparts - they export a load function, receive the same event arguments, and return a plain object.

The key difference is scope. A page load function’s data is available only to that specific page. A layout load function’s data is available to the layout itself and to every page and nested layout within that route segment. Data flows downward through the tree automatically.

Loading diagram...

When SvelteKit renders a page, it calls all applicable load functions ( from the root layout down through any nested layouts to the page itself ) and merges all of their return values together. Each +layout.svelte and +page.svelte then receives whatever data is relevant to its own level and everything above it.


Implementation

Step 1: Add a Load Function to a Layout

The file pairing convention is identical to pages. Where +layout.svelte provides the UI shell, +layout.ts or +layout.server.ts provides the data that shell needs. SvelteKit connects them automatically by filename.

Here is the blog section layout with its load function:

// src/routes/blog/+layout.server.ts

import type { LayoutServerLoad } from './$types'
import { error } from '@sveltejs/kit'

export const load: LayoutServerLoad = async ({ fetch }) => {
	const res = await fetch('/api/posts')
	if (!res.ok) error(res.status, 'Failed to load posts')

	const posts: Array<{ title: string; slug: string; publishedAt: string }> = await res.json()

	return { posts }
}

Notice the type used here: LayoutServerLoad, not PageServerLoad. SvelteKit generates both from ./$types, and they are distinct types because layout load functions and page load functions have slightly different available inputs and different implications for how their data is shared. Always use the layout-specific type for layout files.

<!-- src/routes/blog/+layout.svelte -->

<script lang="ts">
	import type { LayoutProps } from './$types'

	let { data, children }: LayoutProps = $props()
</script>

<div class="blog-shell">
	<aside class="sidebar">
		<h2>All Posts</h2>
		<nav>
			{#each data.posts as post (post.slug)}
				<a href="/blog/{post.slug}">{post.title}</a>
			{/each}
		</nav>
	</aside>

	<main>
		{@render children()}
	</main>
</div>

The children snippet is how Svelte 5 renders nested content inside a layout. When a user visits /blog/my-post, SvelteKit renders the blog layout’s +layout.svelte and places the post’s +page.svelte where {@render children()} appears. The layout wraps the page; the sidebar is always there, and the post content changes per route.

Two things in the layout component are worth examining. First, LayoutProps is imported from ./$types; this is the layout equivalent of PageProps, and it types both data and children appropriately. Second, data.posts is correctly typed as the array returned by the layout load function, with full autocomplete for post.title, post.slug, and post.publishedAt.

Step 2: Understand How Data Reaches the Page

Now consider the individual post page. It has its own load function that fetches the specific post, but the data prop it receives in +page.svelte contains more than just that post:

// src/routes/blog/[slug]/+page.server.ts

import type { PageServerLoad } from './$types'
import { error } from '@sveltejs/kit'

export const load: PageServerLoad = async ({ params, fetch }) => {
	const res = await fetch(`/api/posts/${params.slug}`)

	if (!res.ok) error(res.status, 'Post not found')

	const post = await res.json()
	return { post }
}
<!-- src/routes/blog/[slug]/+page.svelte -->

<script lang="ts">
	import type { PageProps } from './$types'
	import { page } from '$app/state'

	let { data }: PageProps = $props()

	// data.post comes from this page's own load function
	// data.posts comes from the parent blog layout's load function
	let currentIndex = $derived(data.posts.findIndex((p) => p.slug === page.params.slug))
	let nextPost = $derived(data.posts[currentIndex + 1])
</script>

<article>
	<h1>{data.post.title}</h1>
	<div class="content">{@html data.post.content}</div>
</article>

{#if nextPost}
	<nav class="pagination">
		<a href="/blog/{nextPost.slug}">Next: {nextPost.title}</a>
	</nav>
{/if}

This is the important part: the page component accesses data.posts. That data came from the parent layout’s load function, not from its own. SvelteKit silently merges data from every load function in the active layout chain before any component sees it:

Loading diagram...

SvelteKit merges everything together before the component receives it. From the page component’s perspective, data is a single flat object containing all data from all load functions up the tree.

This merge is why data.posts works in +page.svelte even though the page’s own load function never touched posts. The post page can implement the “next post” navigation purely from data that its parent layout already fetched, so no additional network request is required.

Step 3: Handle Key Collisions in Merged Data

When multiple load functions in the same layout tree return data, SvelteKit merges all their return values into a single object for each component. This is convenient, but it means key naming matters. If two load functions return a key with the same name, the one closer to the page wins.

The official rule is: the last one in the tree wins.

// src/routes/+layout.server.ts
export const load = async ({ fetch }) => {
	const res = await fetch('/api/user/me')
	const user = await res.json()

	return {
		user,
		title: 'My Blog' // ← this key
	}
}
// src/routes/blog/[slug]/+page.server.ts
export const load: PageServerLoad = async ({ params, fetch }) => {
	const res = await fetch(`/api/posts/${params.slug}`)
	const post = await res.json()

	return {
		post,
		title: params.slug // ← same key, page wins
	}
}

In this scenario, data.title inside +page.svelte will be the slug value, not 'My Blog'. The root layout’s title is shadowed. This is useful when a page needs to override something set by a parent, but it can be surprising if it happens accidentally.

The solution is deliberate key naming: use specific, unambiguous names rather than generic ones like data, title, or items. In a real application, you might namespace them: layoutTitle, pageTitle, or better yet, structure them as nested objects: { meta: { title: 'My Blog' } } vs { post: { title: params.slug } }.

Step 4: Access Page Data from a Parent Layout

Everything above describes data flowing downward, from parent layouts to child pages. Occasionally you need data to flow in the other direction. The most common case is a root layout that wants to set a dynamic <title> tag based on data returned by whatever page is currently active.

SvelteKit supports this through page.data, exported from $app/state:

<!-- src/routes/+layout.svelte -->

<script lang="ts">
	import { page } from '$app/state'

	let { children } = $props()
</script>

<svelte:head>
	<!-- page.data contains the merged data of the currently active page -->
	<title>{page.data.title ?? 'My Blog'}</title>
</svelte:head>

<header>
	<a href="/">My Blog</a>
</header>

{@render children()}
// src/routes/blog/[slug]/+page.server.ts

export const load: PageServerLoad = async ({ params, fetch }) => {
	const res = await fetch(`/api/posts/${params.slug}`)
	const post = await res.json()

	return {
		post,
		title: post.title // ← this reaches the root layout via page.data
	}
}

page.data is reactive. When the user navigates to a different page, page.data updates to reflect the new page’s merged data, and the <title> updates automatically. This is how SvelteKit enables dynamic document titles without any prop-drilling or global stores.

The type of page.data is defined by App.PageData in your application’s type declarations. SvelteKit does not automatically infer this union type because it cannot know at compile time which page will be active at runtime; the root layout has to handle the uncertainty with a sensible default, as shown with the nullish coalescing ?? 'My Blog' above.


Common Mistakes and Anti-Patterns

Duplicating Shared Data Across Page Load Functions

When developers first encounter the need for shared data, a common reaction is to fetch it in every page load function that needs it. This feels safe; each page is self-contained, but it defeats the entire purpose of the layout loading system.

// AVOID: Fetching shared data in every page

// src/routes/blog/[slug]/+page.server.ts
export const load: PageServerLoad = async ({ params, fetch }) => {
	const [postRes, postsRes] = await Promise.all([
		fetch(`/api/posts/${params.slug}`),
		fetch('/api/posts') // ← fetched here
	])
	return {
		post: await postRes.json(),
		posts: await postsRes.json()
	}
}

// src/routes/blog/+page.server.ts (the blog index page)
export const load: PageServerLoad = async ({ fetch }) => {
	const res = await fetch('/api/posts') // ← fetched again
	return { posts: await res.json() }
}
// PREFERRED: Fetch shared data once in the layout

// src/routes/blog/+layout.server.ts
export const load: LayoutServerLoad = async ({ fetch }) => {
	const res = await fetch('/api/posts')
	return { posts: await res.json() }
}

// src/routes/blog/[slug]/+page.server.ts
export const load: PageServerLoad = async ({ params, fetch }) => {
	const res = await fetch(`/api/posts/${params.slug}`)
	if (res.status === 404) error(404, 'Post not found')
	return { post: await res.json() } // ← posts is already available via the layout
}

Layout load functions are not re-called on every navigation between child routes if their dependencies have not changed. When a user navigates from one blog post to another, SvelteKit recognises that nothing the blog layout load function depends on has changed, so it reuses the cached result. Every individual page load that duplicated this data would run again unnecessarily.

Using the Wrong Type for Layout vs Page Load Functions

The generated types for layout and page load functions are distinct, and using the wrong one will cause TypeScript errors or, worse, silently incorrect behaviour.

// AVOID: Using PageServerLoad in a layout file

// src/routes/blog/+layout.server.ts
import type { PageServerLoad } from './$types' // ← wrong import

export const load: PageServerLoad = async ({ fetch }) => {
	const res = await fetch('/api/posts')
	return { posts: await res.json() }
}
// PREFERRED: Using LayoutServerLoad in a layout file

// src/routes/blog/+layout.server.ts
import type { LayoutServerLoad } from './$types' // ← correct

export const load: LayoutServerLoad = async ({ fetch }) => {
	const res = await fetch('/api/posts')
	return { posts: await res.json() }
}

Similarly, in the layout component itself, use LayoutProps rather than PageProps. The distinction matters because LayoutProps includes the children snippet type that PageProps does not have; page components do not render nested routes.

Forgetting That children Is Required in Layout Components

Every layout component that wraps child routes must render its children snippet. If you omit {@render children()}, child pages simply will not appear. This is obvious when the layout file is new, but easy to accidentally break when refactoring a layout’s template.

<!-- AVOID: Layout that swallows its children -->

<script lang="ts">
	import type { LayoutProps } from './$types'
	let { data }: LayoutProps = $props()
	// children is destructured but never used
</script>

<aside>
	{#each data.posts as post}
		<a href="/blog/{post.slug}">{post.title}</a>
	{/each}
</aside>
<!-- The page content will never appear -->
<!-- PREFERRED: Layout that renders its children -->

<script lang="ts">
	import type { LayoutProps } from './$types'
	let { data, children }: LayoutProps = $props()
</script>

<aside>
	{#each data.posts as post}
		<a href="/blog/{post.slug}">{post.title}</a>
	{/each}
</aside>

<main>
	{@render children()}
</main>

Always destructure children from $props() and always call {@render children()} somewhere in the template. There is no SvelteKit warning when a layout silently drops its children, so this mistake can be confusing to debug.


Performance and Scaling Considerations

Layout load functions are one of SvelteKit’s most significant performance tools, but only when used with an understanding of how dependency tracking works.

SvelteKit tracks what each load function accesses during execution. A layout load function that fetches all posts and returns them does not depend on params, so it always returns the same data regardless of which specific post the user is viewing. When the user navigates from one post to another, SvelteKit sees that the blog layout’s load function has no relevant dependencies that changed, and skips calling it entirely. The data from the previous call is reused.

This means the post list in the sidebar updates only when the list actually changes; not on every navigation. For a blog with hundreds of posts, that is potentially dozens of saved database queries per user session.

The implication for how you structure your layout load functions is that you should keep them appropriately scoped. A root layout load function that fetches data needed by every route in the application is fine. A root layout load function that fetches data only needed by one specific section of the application wastes that data on every route that does not need it, and makes that data available in page.data even where it is irrelevant.

When you have nested layouts, think of each level as having a natural scope. Root layout: user identity and global navigation. Section layout: section-specific navigation and shared reference data. Page: the specific content for this exact view. Respecting those scopes keeps load functions lean and makes the dependency tracking system work in your favour.


When NOT to Use This Pattern

Layout load functions are excellent for data that is genuinely shared across all routes within a layout’s scope. They are the wrong tool for data that is shared through a mechanism other than the route hierarchy.

If you need data to be shared between sibling routes that do not share a common layout ancestor, for instance between /dashboard/stats and /settings/profile, a layout load function would have to live at the root, making that data available everywhere even where it is irrelevant. A better solution for cross-cutting shared data is SvelteKit’s locals object, populated in a server hook and available in any server load function that needs it.

Layout load functions also should not be used to work around the absence of a proper data architecture. If your application has a large number of API calls that all “feel shared”, the solution is usually to think more carefully about which data is genuinely layout-scoped versus which data should be fetched on demand or managed as client-side state.


Conclusion

SvelteKit’s layout load functions extend the same mental model as page load functions - export a load function, return a plain object, receive it as data and apply it at every level of the route hierarchy.

Data from parent layouts flows downward to all child pages automatically. Key collisions between levels resolve in favour of the page. And when page-level data needs to flow upward, such as to a root layout that sets a dynamic title, page.data from $app/state provides a clean, reactive path.

The combination of page and layout loading creates a complete data pipeline where every component at every level of the tree has exactly the data it needs, fetched at the right scope, without redundancy and without manual wiring between components.


Key Takeaways

Layout load functions follow exactly the same convention as page load functions: a load export from +layout.ts or +layout.server.ts, typed with LayoutLoad or LayoutServerLoad from ./$types. The data they return is available to the layout component and to every nested page and layout within that route segment.

SvelteKit merges all load function return values into a single data object at each level of the tree. If two load functions return the same key, the one closer to the page wins. Deliberate key naming prevents accidental shadowing.

Layout load functions are not re-called when the dependency they track has not changed. This makes them an efficient place for data shared across many routes; the query runs once and is reused across navigations within that layout’s scope.

page.data from $app/state provides reactive access to the currently active page’s merged data, enabling patterns like dynamic document titles in a root layout without prop-drilling or stores.


What’s Next

So far, all of the load functions in this series have run in both environments: on the server during SSR and in the browser during client-side navigation. That is the default behaviour of +page.ts and +layout.ts. But some load functions need to run exclusively on the server: database queries, private API keys, session cookies.

SvelteKit handles this with a separate file convention, and understanding when to use each is one of the most important decisions in SvelteKit application architecture. The next article, The page Object from $app/state, covers that reactive object in full — all of its properties, how it updates during client-side navigation, and the patterns that depend on it throughout your application.


Further Reading