Universal Layout Data Loading

Just like pages have +page.js for data loading, layouts have +layout.js. This file loads data that’s needed by the layout itself and flows down to every child page and layout in its subtree. It runs on both server and browser, making it universal data loading.

The key insight about layout load functions is when they run versus when they don’t. A layout’s load function runs the first time you enter its subtree. It does not automatically re-run as you navigate between pages that share that layout — SvelteKit caches the result and reuses it. This makes layout loading efficient for data that is stable across a user’s session within a section: navigation structure, site configuration, available categories. For data that changes frequently or depends on the current URL, you’ll need to express that dependency explicitly (covered in the invalidation section below).

Layout data loading: scope carefully

Layout data loading establishes the “ambient context” for a section of your app: navigation structure, feature flags, user preferences, site configuration. Think carefully about what belongs here versus in individual page load functions. The bar for layout data should be: “does almost every page in this section need this?”


When to Use +layout.js

Use +layout.js when you need to load:

  • Navigation data — Categories, menu items, site structure
  • Public configuration — Feature flags, site settings
  • Shared content — Blog categories, product categories
  • Data needed by multiple pages — Avoid loading the same data in every page

The key requirement: the data must be public (no secrets) and serializable.


Basic Layout Data Loading

// src/routes/blog/+layout.ts
import type { LayoutLoad } from './$types'

export const load: LayoutLoad = async ({ fetch }) => {
	const response = await fetch('/api/categories')
	const categories = await response.json()

	return {
		categories
	}
}

This data is now available in:

  • The layout component (+layout.svelte)
  • All child pages
  • All child layouts

Data Flows Downstream

Superpower: Data returned from a layout’s load function flows down like a waterfall. It’s automatically available to all descendants.

src/routes/
├── +layout.ts Returns { siteConfig }
├── +layout.svelte Has access to data.siteConfig
├── blog/
   ├── +layout.ts Returns { categories }
   ├── +layout.svelte Has access to data.siteConfig AND data.categories
   └── [slug]/
       └── +page.svelte Has access to ALL parent data

Accessing Parent Data

<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
	import type { PageProps } from './$types'

	let { data }: PageProps = $props()

	// data includes:
	// - siteConfig (from root +layout.ts)
	// - categories (from blog/+layout.ts)
	// - post (from [slug]/+page.ts)
</script>

<aside>
	<h3>Categories</h3>
	{#each data.categories as category}
		<a href="/blog?category={category.slug}">{category.name}</a>
	{/each}
</aside>

<article>
	<h1>{data.post.title}</h1>
	<p>{data.post.content}</p>
</article>

Accessing Parent Data in Load Functions

Child load functions can access parent data using the parent() function:

// src/routes/blog/[slug]/+page.ts
import type { PageLoad } from './$types'

export const load: PageLoad = async ({ params, parent }) => {
	// Wait for parent layout data
	const parentData = await parent()

	// Now you can use parentData.categories, etc.
	const category = parentData.categories.find((c) => c.id === params.categoryId)

	return {
		post: await fetchPost(params.slug),
		categoryName: category?.name
	}
}
Use `parent()` judiciously

Call parent() only when you genuinely need data from a parent layout. Calling it unnecessarily forces this load function to wait for the parent to finish before it can start — even if the data it produces has nothing to do with the parent result. If you can fetch your data independently and just need a small piece of parent context (like a locale string or a category list), consider whether that piece could be in params, url, or the route itself instead. Unnecessary parent() calls are one of the most common causes of avoidable data-loading waterfalls in SvelteKit apps.


URL-Based Data Loading

Layout load functions have access to the URL, allowing you to load different data based on query parameters:

// src/routes/blog/+layout.ts
import type { LayoutLoad } from './$types'

export const load: LayoutLoad = async ({ fetch, url }) => {
	const activeCategory = url.searchParams.get('category')

	const [categories, featuredPosts] = await Promise.all([
		fetch('/api/categories').then((r) => r.json()),
		fetch(`/api/posts/featured?category=${activeCategory || ''}`).then((r) => r.json())
	])

	return {
		categories,
		featuredPosts,
		activeCategory
	}
}

When Layout Data Reloads

This is one of the most important behaviours to understand, and a common source of confusion.

By default, a layout’s load function runs once when you enter the layout’s subtree. It does not re-run when navigating between sibling pages within that subtree. Navigating from /blog/post-a to /blog/post-b will not re-run /blog/+layout.ts. That’s intentional — reloading navigation categories or site configuration on every page click would be wasteful.

But sometimes you do need the layout to re-run. If your layout load function fetches a filtered list based on a URL query parameter, it needs to re-run when that parameter changes.

The way to express this in SvelteKit is by reading from the url object or calling depends(). SvelteKit tracks what a load function accessed during its last run. If the URL changes in a way that would produce different inputs, the load function re-runs:

// src/routes/blog/+layout.ts
import type { LayoutLoad } from './$types'

export const load: LayoutLoad = async ({ fetch, url, depends }) => {
	// Declaring a custom dependency allows manual invalidation via invalidate('app:category')
	depends('app:category')

	// Reading url.searchParams causes SvelteKit to re-run this function
	// whenever the ?category= query string changes
	const category = url.searchParams.get('category')

	return {
		posts: await fetch(`/api/posts?category=${category}`).then((r) => r.json())
	}
}

Because this load function reads url.searchParams, SvelteKit knows it depends on the URL query string. Changing from /blog?category=tech to /blog?category=design will re-run the layout load function automatically. Without accessing the url object, the load function would only run once.

To trigger a re-run from anywhere in your app, call invalidate('app:category') or use invalidateAll() for a full re-fetch of all load functions on the current page.


Parallel Data Loading

When loading multiple resources, use Promise.all for efficiency:

// src/routes/+layout.ts
import type { LayoutLoad } from './$types'

export const load: LayoutLoad = async ({ fetch }) => {
	// Load all resources in parallel
	const [navigation, footerLinks, announcements] = await Promise.all([
		fetch('/api/navigation').then((r) => r.json()),
		fetch('/api/footer-links').then((r) => r.json()),
		fetch('/api/announcements').then((r) => r.json())
	])

	return {
		navigation,
		footerLinks,
		announcements
	}
}

Combining with +layout.server.js

You can have both +layout.js and +layout.server.js for the same layout. When you do:

  1. +layout.server.js runs first (on the server)
  2. Its data is available to +layout.js as the data property
  3. +layout.js can transform or add to this data
// src/routes/+layout.server.ts
export const load = async ({ locals }) => {
	return {
		user: locals.user // Server-only (from cookies/session)
	}
}
// src/routes/+layout.ts
import type { LayoutLoad } from './$types'

export const load: LayoutLoad = async ({ data, fetch }) => {
	// `data` contains { user } from +layout.server.ts

	const navigation = await fetch('/api/navigation').then((r) => r.json())

	return {
		...data, // Include server data
		navigation // Add universal data
	}
}

Common Patterns

Site-Wide Configuration

// src/routes/+layout.ts
import type { LayoutLoad } from './$types'

export const load: LayoutLoad = async ({ fetch }) => {
	const config = await fetch('/api/config').then((r) => r.json())

	return {
		siteName: config.siteName,
		features: {
			darkMode: config.enableDarkMode,
			newsletter: config.enableNewsletter
		}
	}
}
// src/routes/+layout.ts
import type { LayoutLoad } from './$types'

export const load: LayoutLoad = async ({ url }) => {
	const navigation = [
		{ href: '/', label: 'Home' },
		{ href: '/about', label: 'About' },
		{ href: '/blog', label: 'Blog' },
		{ href: '/contact', label: 'Contact' }
	].map((item) => ({
		...item,
		active: url.pathname === item.href || url.pathname.startsWith(item.href + '/')
	}))

	return { navigation }
}

Conclusion

The +layout.js file represents a powerful pattern for establishing application-wide context and shared data without the overhead of redundant requests or prop drilling.

By loading data once at the layout level and making it available to all descendant routes, you create a more efficient, maintainable architecture where navigation structure, user preferences, and shared configuration flow naturally through your component hierarchy.

The universal nature of this file (running on both server and client) ensures consistent behavior whether users arrive via direct URL or client-side navigation.

Mastering +layout.js requires understanding its relationship with +layout.server.js for hybrid data strategies, the parent() function for accessing ancestor data, and invalidation patterns for controlling when layouts re-run.

By thoughtfully organizing what data belongs in layouts versus pages, you build applications that are both performant and easy to reason about—with clear boundaries between shared context and page-specific concerns.

Key Takeaways

  • +layout.js provides universal data loading for layouts that runs on both server (SSR) and client (navigation), making data available to the layout component and all child routes
  • Data flows downstream to all descendants - child pages and layouts automatically receive parent layout data through the data prop without explicit prop passing
  • The parent() function enables data composition - child load functions can await parent() to access ancestor data and build upon it with additional properties
  • Layout data is cached during navigation between sibling routes - data only reloads when navigating away from and back to the layout’s scope or when explicitly invalidated
  • Invalidation patterns control re-runs - use depends('custom:identifier') with invalidate() or include URL-based dependencies to trigger layout data refresh
  • Combine with +layout.server.js for hybrid strategies - use .server.js for sensitive data (auth, database) and .js for public data (navigation, categories)
  • Parallel loading optimizes performance - use Promise.all() to fetch multiple data sources concurrently rather than sequentially
  • Must return serializable data - all data returned from +layout.js must be JSON-serializable since it flows from server to client during SSR

See Also