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 carefullyLayout 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()` judiciouslyCall
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 inparams,url, or the route itself instead. Unnecessaryparent()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:
+layout.server.jsruns first (on the server)- Its data is available to
+layout.jsas thedataproperty +layout.jscan 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
}
}
} Navigation with Active State
// 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.jsprovides 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
dataprop without explicit prop passing - The
parent()function enables data composition - child load functions can awaitparent()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')withinvalidate()or include URL-based dependencies to trigger layout data refresh - Combine with
+layout.server.jsfor hybrid strategies - use.server.jsfor sensitive data (auth, database) and.jsfor 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.jsmust be JSON-serializable since it flows from server to client during SSR
See Also
- Official SvelteKit Documentation - +layout.js
- SvelteKit Load Functions - Comprehensive load function guide
invalidate()andinvalidateAll()- Manual data invalidation