Skip to main content

Loading data

Edit this page on GitHub

Before a +page.svelte component (and its containing +layout.svelte components) can be rendered, we often need to get some data. This is done by defining load functions.

Page data

A +page.svelte file can have a sibling +page.js (or +page.ts) that exports a load function, the return value of which is available to the page via the data prop:

src/routes/blog/[slug]/+page.js
ts
/** @type {import('./$types').PageLoad} */
export function load({ params }) {
return {
post: {
title: `Title for ${params.slug} goes here`,
content: `Content for ${params.slug} goes here`
}
};
}
src/routes/blog/[slug]/+page.ts
ts
import type { PageLoad } from './$types';
 
export const load = (({ params }) => {
return {
post: {
title: `Title for ${params.slug} goes here`,
content: `Content for ${params.slug} goes here`
}
};
}) satisfies PageLoad;
src/routes/blog/[slug]/+page.svelte
<script>
  /** @type {import('./$types').PageData} */  export let data;
</script>

<h1>{data.post.title}</h1>
<div>{@html data.post.content}</div>
src/routes/blog/[slug]/+page.svelte
<script lang="ts">
  import type { PageData } from './$types';

  export let data: PageData;
</script>

<h1>{data.post.title}</h1>
<div>{@html data.post.content}</div>

Thanks to the generated $types module, we get full type safety.

A load function in a +page.js file runs both on the server and in the browser. If your load function should always run on the server (because it uses private environment variables, for example, or accesses a database) then it would go in a +page.server.js instead.

A more realistic version of your blog post's load function, that only runs on the server and pulls data from a database, might look like this:

src/routes/blog/[slug]/+page.server.js
ts
import * as db from '$lib/server/database';
 
/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
return {
post: await db.getPost(params.slug)
};
}
src/routes/blog/[slug]/+page.server.ts
ts
import * as db from '$lib/server/database';
import type { PageServerLoad } from './$types';
 
export const load = (async ({ params }) => {
return {
post: await db.getPost(params.slug)
};
}) satisfies PageServerLoad;

Notice that the type changed from PageLoad to PageServerLoad, because server load functions can access additional arguments. To understand when to use +page.js and when to use +page.server.js, see Universal vs server.

Layout data

Your +layout.svelte files can also load data, via +layout.js or +layout.server.js.

src/routes/blog/[slug]/+layout.server.js
ts
import * as db from '$lib/server/database';
 
/** @type {import('./$types').LayoutServerLoad} */
export async function load() {
return {
posts: await db.getPostSummaries()
};
}
src/routes/blog/[slug]/+layout.server.ts
ts
import * as db from '$lib/server/database';
import type { LayoutServerLoad } from './$types';
 
export const load = (async () => {
return {
posts: await db.getPostSummaries()
};
}) satisfies LayoutServerLoad;
src/routes/blog/[slug]/+layout.svelte
<script>
  /** @type {import('./$types').LayoutData} */  export let data;
</script>

<main>
  <!-- +page.svelte is rendered in this <slot> -->  <slot />
</main>

<aside>
  <h2>More posts</h2>
  <ul>
    {#each data.posts as post}
      <li>
        <a href="/blog/{post.slug}">
          {post.title}
        </a>
      </li>
    {/each}
  </ul>
</aside>
src/routes/blog/[slug]/+layout.svelte
<script lang="ts">
  import type { LayoutData } from './$types';

  export let data: LayoutData;
</script>

<main>
  <!-- +page.svelte is rendered in this <slot> -->  <slot />
</main>

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

Data returned from layout load functions is available to child +layout.svelte components and the +page.svelte component as well as the layout that it 'belongs' to.

src/routes/blog/[slug]/+page.svelte
<script>
	import { page } from '$app/stores';

  /** @type {import('./$types').PageData} */
  export let data;

	// we can access `data.posts` because it's returned from
	// the parent layout `load` function
	$: index = data.posts.findIndex(post => post.slug === $page.params.slug);
	$: next = data.posts[index - 1];
</script>

<h1>{data.post.title}</h1>
<div>{@html data.post.content}</div>

{#if next}
	<p>Next post: <a href="/blog/{next.slug}">{next.title}</a></p>
{/if}

If multiple load functions return data with the same key, the last one 'wins' — the result of a layout load returning { a: 1, b: 2 } and a page load returning { b: 3, c: 4 } would be { a: 1, b: 3, c: 4 }.

$page.data

The +page.svelte component, and each +layout.svelte component above it, has access to its own data plus all the data from its parents.

In some cases, we might need the opposite — a parent layout might need to access page data or data from a child layout. For example, the root layout might want to access a title property returned from a load function in +page.js or +page.server.js. This can be done with $page.data:

src/routes/+layout.svelte
<script>
  import { page } from '$app/stores';
</script>

<svelte:head>
  <title>{$page.data.title}</title>
</svelte:head>

Type information for $page.data is provided by App.PageData.

Universal vs server

As we've seen, there are two types of load function:

  • +page.js and +layout.js files export universal load functions that run both on the server and in the browser
  • +page.server.js and +layout.server.js files export server load functions that only run server-side

Conceptually, they're the same thing, but there are some important differences to be aware of.

Input

Both universal and server load functions have access to properties describing the request (params, route and url) and various functions (fetch, setHeaders, parent and depends). These are described in the following sections.

Server load functions are called with a ServerLoadEvent, which inherits clientAddress, cookies, locals, platform and request from RequestEvent.

Universal load functions are called with a LoadEvent, which has a data property. If you have load functions in both +page.js and +page.server.js (or +layout.js and +layout.server.js), the return value of the server load function is the data property of the universal load function's argument.

Output

A universal load function can return an object containing any values, including things like custom classes and component constructors.

A server load function must return data that can be serialized with devalue — anything that can be represented as JSON plus things like BigInt, Date, Map, Set and RegExp, or repeated/cyclical references — so that it can be transported over the network.

When to use which

Server load functions are convenient when you need to access data directly from a database or filesystem, or need to use private environment variables.

Universal load functions are useful when you need to fetch data from an external API and don't need private credentials, since SvelteKit can get the data directly from the API rather than going via your server. They are also useful when you need to return something that can't be serialized, such as a Svelte component constructor.

In rare cases, you might need to use both together — for example, you might need to return an instance of a custom class that was initialised with data from your server.

Using URL data

Often the load function depends on the URL in one way or another. For this, the load function provides you with url, route and params.

url

An instance of URL, containing properties like the origin, hostname, pathname and searchParams (which contains the parsed query string as a URLSearchParams object). url.hash cannot be accessed during load, since it is unavailable on the server.

In some environments this is derived from request headers during server-side rendering. If you're using adapter-node, for example, you may need to configure the adapter in order for the URL to be correct.

route

Contains the name of the current route directory, relative to src/routes:

src/routes/a/[b]/[...c]/+page.js
ts
/** @type {import('./$types').PageLoad} */
export function load({ route }) {
console.log(route.id); // '/a/[b]/[...c]'
}
src/routes/a/[b]/[...c]/+page.ts
ts
import type { PageLoad } from './$types';
 
export const load = (({ route }) => {
console.log(route.id); // '/a/[b]/[...c]'
}) satisfies PageLoad;

params

params is derived from url.pathname and route.id.

Given a route.id of /a/[b]/[...c] and a url.pathname of /a/x/y/z, the params object would look like this:

{
  "b": "x",
  "c": "y/z"
}

Making fetch requests

To get data from an external API or a +server.js handler, you can use the provided fetch function, which behaves identically to the native fetch web API with a few additional features:

  • it can be used to make credentialed requests on the server, as it inherits the cookie and authorization headers for the page request
  • it can make relative requests on the server (ordinarily, fetch requires a URL with an origin when used in a server context)
  • internal requests (e.g. for +server.js routes) go direct to the handler function when running on the server, without the overhead of an HTTP call
  • during server-side rendering, the response will be captured and inlined into the rendered HTML. Note that headers will not be serialized, unless explicitly included via filterSerializedResponseHeaders. Then, during hydration, the response will be read from the HTML, guaranteeing consistency and preventing an additional network request - if you got a warning in your browser console when using the browser fetch instead of the load fetch, this is why.
src/routes/items/[id]/+page.js
ts
/** @type {import('./$types').PageLoad} */
export async function load({ fetch, params }) {
const res = await fetch(`/api/items/${params.id}`);
const item = await res.json();
 
return { item };
}
src/routes/items/[id]/+page.ts
ts
import type { PageLoad } from './$types';
 
export const load = (async ({ fetch, params }) => {
const res = await fetch(`/api/items/${params.id}`);
const item = await res.json();
 
return { item };
}) satisfies PageLoad;

Cookies will only be passed through if the target host is the same as the SvelteKit application or a more specific subdomain of it.

Cookies and headers

A server load function can get and set cookies.

src/routes/+layout.server.js
ts
import * as db from '$lib/server/database';
 
/** @type {import('./$types').LayoutServerLoad} */
export async function load({ cookies }) {
const sessionid = cookies.get('sessionid');
 
return {
user: await db.getUser(sessionid)
};
}
src/routes/+layout.server.ts
ts
import * as db from '$lib/server/database';
import type { LayoutServerLoad } from './$types';
 
export const load = (async ({ cookies }) => {
const sessionid = cookies.get('sessionid');
 
return {
user: await db.getUser(sessionid)
};
}) satisfies LayoutServerLoad;

When setting cookies, be aware of the path property. By default, the path of a cookie is the current pathname. If you for example set a cookie at page admin/user, the cookie will only be available within the admin pages by default. In most cases you likely want to set path to '/' to make the cookie available throughout your app.

Both server and universal load functions have access to a setHeaders function that, when running on the server, can set headers for the response. (When running in the browser, setHeaders has no effect.) This is useful if you want the page to be cached, for example:

src/routes/products/+page.js
ts
/** @type {import('./$types').PageLoad} */
export async function load({ fetch, setHeaders }) {
const url = `https://cms.example.com/products.json`;
const response = await fetch(url);
 
// cache the page for the same length of time
// as the underlying data
setHeaders({
age: response.headers.get('age'),
'cache-control': response.headers.get('cache-control')
});
 
return response.json();
}
src/routes/products/+page.ts
ts
import type { PageLoad } from './$types';
 
export const load = (async ({ fetch, setHeaders }) => {
const url = `https://cms.example.com/products.json`;
const response = await fetch(url);
 
// cache the page for the same length of time
// as the underlying data
setHeaders({
age: response.headers.get('age'),
'cache-control': response.headers.get('cache-control')
});
 
return response.json();
}) satisfies PageLoad;

Setting the same header multiple times (even in separate load functions) is an error — you can only set a given header once. You cannot add a set-cookie header with setHeaders — use cookies.set(name, value, options) instead.

Using parent data

Occasionally it's useful for a load function to access data from a parent load function, which can be done with await parent():

src/routes/+layout.js
ts
/** @type {import('./$types').LayoutLoad} */
export function load() {
return { a: 1 };
}
src/routes/+layout.ts
ts
import type { LayoutLoad } from './$types';
 
export const load = (() => {
return { a: 1 };
}) satisfies LayoutLoad;
src/routes/abc/+layout.js
ts
/** @type {import('./$types').LayoutLoad} */
export async function load({ parent }) {
const { a } = await parent();
return { b: a + 1 };
}
src/routes/abc/+layout.ts
ts
import type { LayoutLoad } from './$types';
 
export const load = (async ({ parent }) => {
const { a } = await parent();
return { b: a + 1 };
}) satisfies LayoutLoad;
src/routes/abc/+page.js
ts
/** @type {import('./$types').PageLoad} */
export async function load({ parent }) {
const { a, b } = await parent();
return { c: a + b };
}
src/routes/abc/+page.ts
ts
import type { PageLoad } from './$types';
 
export const load = (async ({ parent }) => {
const { a, b } = await parent();
return { c: a + b };
}) satisfies PageLoad;
<script>
  /** @type {import('./$types').PageData} */  export let data;
</script>
<!-- renders `1 + 2 = 3` --><p>{data.a} + {data.b} = {data.c}</p>

Notice that the load function in +page.js receives the merged data from both layout load functions, not just the immediate parent.

Inside +page.server.js and +layout.server.js, parent returns data from parent +layout.server.js files.

In +page.js or +layout.js it will return data from parent +layout.js files. However, a missing +layout.js is treated as a ({ data }) => data function, meaning that it will also return data from parent +layout.server.js files that are not 'shadowed' by a +layout.js file

Take care not to introduce waterfalls when using await parent(). Here, for example, getData(params) does not depend on the result of calling parent(), so we should call it first to avoid a delayed render.

+page.js
/** @type {import('./$types').PageLoad} */
export async function load({ params, parent }) {
	const parentData = await parent();
  const data = await getData(params);
	const parentData = await parent();

  return {
    ...data
    meta: { ...parentData.meta, ...data.meta }
  };
}

Errors

If an error is thrown during load, the nearest +error.svelte will be rendered. For expected errors, use the error helper from @sveltejs/kit to specify the HTTP status code and an optional message:

src/routes/admin/+layout.server.js
ts
import { error } from '@sveltejs/kit';
 
/** @type {import('./$types').LayoutServerLoad} */
export function load({ locals }) {
if (!locals.user) {
throw error(401, 'not logged in');
}
 
if (!locals.user.isAdmin) {
throw error(403, 'not an admin');
}
}
src/routes/admin/+layout.server.ts
ts
import { error } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';
 
export const load = (({ locals }) => {
if (!locals.user) {
throw error(401, 'not logged in');
}
 
if (!locals.user.isAdmin) {
throw error(403, 'not an admin');
}
}) satisfies LayoutServerLoad;

If an unexpected error is thrown, SvelteKit will invoke handleError and treat it as a 500 Internal Error.

Redirects

To redirect users, use the redirect helper from @sveltejs/kit to specify the location to which they should be redirected alongside a 3xx status code.

src/routes/user/+layout.server.js
ts
import { redirect } from '@sveltejs/kit';
 
/** @type {import('./$types').LayoutServerLoad} */
export function load({ locals }) {
if (!locals.user) {
throw redirect(307, '/login');
}
}
src/routes/user/+layout.server.ts
ts
import { redirect } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';
 
export const load = (({ locals }) => {
if (!locals.user) {
throw redirect(307, '/login');
}
}) satisfies LayoutServerLoad;

Make sure you're not catching the thrown redirect, which results in a noop.

Promise unwrapping

Top-level promises will be awaited, which makes it easy to return multiple promises without creating a waterfall:

src/routes/+page.js
ts
/** @type {import('./$types').PageLoad} */
export function load() {
return {
a: Promise.resolve('a'),
b: Promise.resolve('b'),
c: {
value: Promise.resolve('c')
}
};
}
src/routes/+page.ts
ts
import type { PageLoad } from './$types';
 
export const load = (() => {
return {
a: Promise.resolve('a'),
b: Promise.resolve('b'),
c: {
value: Promise.resolve('c')
}
};
}) satisfies PageLoad;
<script>
  /** @type {import('./$types').PageData} */  export let data;

  console.log(data.a); // 'a'
  console.log(data.b); // 'b'
  console.log(data.c.value); // `Promise {...}`
</script>

Parallel loading

When rendering (or navigating to) a page, SvelteKit runs all load functions concurrently, avoiding a waterfall of requests. During client-side navigation, the result of calling multiple server load functions are grouped into a single response. Once all load functions have returned, the page is rendered.

Invalidation

SvelteKit tracks the dependencies of each load function to avoid re-running it unnecessarily during navigation.

For example, given a pair of load functions like these...

src/routes/blog/[slug]/+page.server.js
ts
import * as db from '$lib/server/database';
 
/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
return {
post: await db.getPost(params.slug)
};
}
src/routes/blog/[slug]/+page.server.ts
ts
import * as db from '$lib/server/database';
import type { PageServerLoad } from './$types';
 
export const load = (async ({ params }) => {
return {
post: await db.getPost(params.slug)
};
}) satisfies PageServerLoad;
src/routes/blog/[slug]/+layout.server.js
ts
import * as db from '$lib/server/database';
 
/** @type {import('./$types').LayoutServerLoad} */
export async function load() {
return {
posts: await db.getPostSummaries()
};
}
src/routes/blog/[slug]/+layout.server.ts
ts
import * as db from '$lib/server/database';
import type { LayoutServerLoad } from './$types';
 
export const load = (async () => {
return {
posts: await db.getPostSummaries()
};
}) satisfies LayoutServerLoad;

...the one in +page.server.js will re-run if we navigate from /blog/trying-the-raw-meat-diet to /blog/i-regret-my-choices because params.slug has changed. The one in +layout.server.js will not, because the data is still valid. In other words, we won't call db.getPostSummaries() a second time.

A load function that calls await parent() will also re-run if a parent load function is re-run.

Manual invalidation

You can also re-run load functions that apply to the current page using invalidate(url), which re-runs all load functions that depend on url, and invalidateAll(), which re-runs every load function.

A load function depends on url if it calls fetch(url) or depends(url). Note that url can be a custom identifier that starts with [a-z]::

src/routes/random-number/+page.js
ts
/** @type {import('./$types').PageLoad} */
export async function load({ fetch, depends }) {
// load reruns when `invalidate('https://api.example.com/random-number')` is called...
const response = await fetch('https://api.example.com/random-number');
 
// ...or when `invalidate('app:random')` is called
depends('app:random');
 
return {
number: await response.json()
};
}
src/routes/random-number/+page.ts
ts
import type { PageLoad } from './$types';
 
export const load = (async ({ fetch, depends }) => {
// load reruns when `invalidate('https://api.example.com/random-number')` is called...
const response = await fetch('https://api.example.com/random-number');
 
// ...or when `invalidate('app:random')` is called
depends('app:random');
 
return {
number: await response.json()
};
}) satisfies PageLoad;
src/routes/random-number/+page.svelte
<script>
  import { invalidate, invalidateAll } from '$app/navigation';
  /** @type {import('./$types').PageData} */  export let data;

  function rerunLoadFunction() {
    // any of these will cause the `load` function to re-run    invalidate('app:random');
    invalidate('https://api.example.com/random-number');
    invalidate(url => url.href.includes('random-number'));
    invalidateAll();
  }
</script>

<p>random number: {data.number}</p>
<button on:click={rerunLoadFunction}>Update random number</button>
src/routes/random-number/+page.svelte
<script lang="ts">
  import { invalidate, invalidateAll } from '$app/navigation';

  import type { PageData } from './$types';

  export let data: PageData;

  function rerunLoadFunction() {
    // any of these will cause the `load` function to re-run    invalidate('app:random');
    invalidate('https://api.example.com/random-number');
    invalidate(url => url.href.includes('random-number'));
    invalidateAll();
  }
</script>

<p>random number: {data.number}</p>
<button on:click={rerunLoadFunction}>Update random number</button>

To summarize, a load function will re-run in the following situations:

  • It references a property of params whose value has changed
  • It references a property of url (such as url.pathname or url.search) whose value has changed
  • It calls await parent() and a parent load function re-ran
  • It declared a dependency on a specific URL via fetch or depends, and that URL was marked invalid with invalidate(url)
  • All active load functions were forcibly re-run with invalidateAll()

Note that re-running a load function will update the data prop inside the corresponding +layout.svelte or +page.svelte; it does not cause the component to be recreated. As a result, internal state is preserved. If this isn't what you want, you can reset whatever you need to reset inside an afterNavigate callback, and/or wrap your component in a {#key ...} block.

Shared state

In many server environments, a single instance of your app will serve multiple users. For that reason, per-request or per-user state must not be stored in shared variables outside your load functions, but should instead be stored in event.locals.

previous Routing