SuperAPI AI Directory

sveltekit-data

Frontend & websites

View repository

Updated 10-16 05:48

About this skill

SvelteKit Data: Loading, Form Actions, Page Options (SvelteKit 2)

This skill covers the three pillars of the SvelteKit data layer: (1) Loading data (+page.js / +page.server.js / +layout.js / +layout.server.js of load function,page.data, URL data, cookies/headers, errors and redirects, streaming responses, dependency tracking and manual invalidation), (2) Form actions (default/named actions,fail validation,use:enhance progressive enhancement,GET vs POST),(3) Page options(prerender / entries / ssr / csr / trailingSlash / config)。

When to use this skill

  • decide the trade-off of +page.js vs +page.server.js (private credentials, serialization, combined use)
  • in load function use params / route / url / fetch / parent / depends / untrack
  • use page.data to access child page data in a parent layout
  • use error(status, msg) / redirect(status, location) to terminate load/action
  • use returning a Promise to implement a streaming skeleton screen
  • solve "when does the load function re-run" and how to use invalidate / invalidateAll / untrack for precise control
  • in server load / action read cookies / set response headers
  • design +page.server.js actions: default action, named action,?/name query parameters,fail() validation
  • use use:enhance to progressively enhance forms, backfill form fields, prevent default reset, customize applyAction
  • decide which routes prerender = true / entries() / ssr = false / csr = false / trailingSlash

Critical: Loading data (universal vs server load functions)

SvelteKit has two kinds of load functions, which run in different locations and have different constraints.

Universal load(+page.js / +layout.js): by default during SSR it runs once on the server, then runs again during client hydration; after that all navigation runs in the browser. It can returnany JS value(including non-serializable objects such as Svelte component constructors).fetch is inlined into HTML during the SSR phase, reused during hydration, and will not leak private credentials to the client.

Server load(+page.server.js / +layout.server.js):always runs on the server. The return value must be serialized with devalue (JSON + BigInt / Date / Map / Set / RegExp / circular references). Can access cookies / locals / request / clientAddress / platform。

// +page.server.js
import * as db from '$lib/server/database';

/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
  return { post: await db.getPost(params.slug) };
}
// +page.js  ——  公共 API fetch,浏览器可直接拉
/** @type {import('./$types').PageLoad} */
export async function load({ fetch, params }) {
  const res = await fetch(`/api/items/${params.id}`);
  return { item: await res.json() };
}

Both can exist at the same time. When both exist, server load runs first, and its return value becomes the data parameter of universal load; only the return value of universal load reaches the page:

// +page.server.js —— 仅在服务端,返回 sessionId
export async function load({ locals }) {
  return { sessionId: locals.sessionId };
}

// +page.js —— 浏览器也能跑,但能拿到服务端上下文
export async function load({ data, fetch }) {
  const res = await fetch(`/api/me`, { headers: { 'x-session': data.sessionId } });
  return { me: await res.json() };
}

When to use which:

  • Need private environment variables, database, file system → server load
  • Data is a public API, and you want to reduce server round trips → universal load
  • Need to return a non-serializable object (Svelte component class) → universal load
  • Need streaming (slow data + skeleton screen) → must be server load(a Promise from universal load will not be streamed)
  • Need to be able to re-run on the client too → universal load

Critical: page.data (sharing data across components)

The page and all ancestor layouts each havetheir own data prop, containing the merged data of itself + all ancestors. Whenparentcomponent needschildcomponent's returned data, use $app/state of page.data($app/stores of $page is the old-style equivalent):

<!-- src/routes/+layout.svelte -->
<script>
  import { page } from '$app/state';
  /** @type {import('./$types').LayoutProps} */
  let { data, children } = $props();
</script>

<svelte:head>
  <title>{page.data.title}</title>
</svelte:head>
{@render children()}

Merge rule: when the same key appears at the same levelthe latter overrides。+layout.js returns { a:1, b:2 } + +page.js returns { b:3, c:4 } → data = { a:1, b:3, c:4 }。

Critical: URL data (params/route/url)

load function via url / route / params Access the URL.url.hash Not available during SSR (no fragment on the server).

// src/routes/a/[b]/[...c]/+page.js
/** @type {import('./$types').PageLoad} */
export function load({ url, route, params }) {
  // route.id    = '/a/[b]/[...c]'
  // url         = URL 实例(origin/pathname/searchParams/...)
  // params.b    = 'x'   params.c = 'y/z'
  return { q: url.searchParams.get('q') };
}

url.searchParams.get/getAll/has In dependency tracking it is anindependentkey——?x=1&y=1 → ?x=1&y=2 will notrerun a load that only depends on y .

Critical: Cookies and Headers

Only server load can read / write cookies。setHeaders takes effect when called during SSR in universal load; calling it in the browser is a no-op.

// +layout.server.js
export async function load({ cookies }) {
  const sessionid = cookies.get('sessionid');
  return { user: await db.getUser(sessionid) };
}

// +page.js —— 转发上游 cache 头
export async function load({ fetch, setHeaders }) {
  const response = await fetch('https://cms.example.com/products.json');
  setHeaders({ 'cache-control': response.headers.get('cache-control') });
  return response.json();
}

Constraints:setHeaders a header with the same name can only be set once; you cannot use setHeaders to set set-cookie(use cookies.set);fetch only onsame-origin or subdomaintargets with cookies; for other domains use handleFetch hook。

Critical: Errors and Redirects

error(status, message) and redirect(status, location) boththrow directly——do not throwyourself (SvelteKit 1.x behavior is deprecated).redirect Do not put it in a try block(it will be swallowed by catch).

import { error, redirect } from '@sveltejs/kit';

export function load({ locals }) {
  if (!locals.user) error(401, 'not logged in');
  if (!locals.user.isAdmin) error(403, 'not an admin');
}

export function load({ locals, url }) {
  if (!locals.user) {
    const next = url.pathname + url.search;
    redirect(303, `/login?redirectTo=${encodeURIComponent(next)}`);
  }
}

expected error(throw with error() ) shows the nearest +error.svelte with the correct status;unexpected error triggers the handleError hook and is handled as 500.

For browser-side navigation use $app/navigation of goto:

import { goto } from '$app/navigation';
goto('/login');

Critical: Streaming with promises

Server load returnsan un-awaited Promise will be streamed to the browser, allowing fast data to render first and slow data to arrive later.

// +page.server.js —— 关键数据先 await,慢数据后流
export async function load({ params }) {
  return {
    post: await loadPost(params.slug),
    comments: loadComments(params.slug)   // 不 await → 单独流
  };
}
<!-- +page.svelte -->
<h1>{data.post.title}</h1>
{#await data.comments}
  <p>Loading comments...</p>
{:then comments}
  {#each comments as c}<p>{c.content}</p>{/each}
{:catch error}
  <p>error: {error.message}</p>
{/await}

Hard constraint:

  • streaming only takes effect when JS is enabled and not on buffering platforms such as Lambda/Firebase
  • After the response starts streamingyou cannotthen setHeaders / redirect
  • manually add .catch(() => {}) to a Promise to prevent unhandled rejection;fetch is handled automatically by SvelteKit
  • A params.x inside a nested promise will not be dependency-tracked——it must be accessed at the top-level body

Critical: When load functions rerun

SvelteKit tracks each load's dependencies to avoid rerunning.load Conditions for re-execution:

  1. Access params a property and its value changed
  2. Access url.pathname / url.search etc. and the value changed
  3. url.searchParams.get/getAll/has the corresponding parameter changed
  4. await parent() and the parent load reran
  5. Declare a dependency via fetch(url) or depends(url) , and invalidate(url) is called
  6. invalidateAll() force rerun all active loads
// +page.js —— 自定义依赖标签(约定 [a-z]: 前缀)
export async function load({ fetch, depends }) {
  depends('app:random');
  const r = await fetch('https://api.example.com/random-number');
  return { number: await r.json() };
}
<script>
  import { invalidate, invalidateAll } from '$app/navigation';
  function rerun() {
    invalidate('app:random');
    invalidate('https://api.example.com/random-number');
    invalidate(url => url.href.includes('random-number'));
    invalidateAll();
  }
</script>
<button onclick={rerun}>refresh</button>

server load does notautomatically depend on the fetched URL (to avoid leaking credentials)——you must depends(url) declare it explicitly. Use untrack(fn) to exclude a dependency:

export async function load({ untrack, url }) {
  if (untrack(() => url.pathname === '/')) return { message: 'Welcome!' };
}

Rerun ≠ rebuild component。+layout.svelte / +page.svelte instance is preserved, only the data prop updates, and the component's internal state is preserved. To force a rebuild → use {#key page.url.pathname}。

Critical: Form actions (default/named, validation, redirects)

+page.server.js export actions object to provide <form> endpoints.action is always POST(GET should not have side effects).

Default action:

// src/routes/login/+page.server.js
/** @satisfies {import('./$types').Actions} */
export const actions = {
  default: async (event) => { /* ... */ }
};
<form method="POST">
  <input name="email">
  <input name="password" type="password">
  <button>Log in</button>
</form>

Named actions: use ?/name to distinguish:

export const actions = {
  login: async (event) => { /* ... */ },
  register: async (event) => { /* ... */ }
};
<form method="POST" action="?/login">...</form>
<form method="POST" action="/login?/register">...</form>  <!-- 跨页调用 -->
<!-- 同表单不同按钮 -->
<form method="POST" action="?/login">
  <button>Login</button>
  <button formaction="?/register">Register</button>
</form>

Important: default + named cannot coexist——if a named POST action does not redirect,?/name stays in the URL, and the next default POST will also hit it.

Validation errors:fail(status, data) returns status + data, and data goes into form prop / page.form / page.status:

import { fail } from '@sveltejs/kit';
export const actions = {
  login: async ({ cookies, request }) => {
    const data = await request.formData();
    const email = data.get('email');
    const password = data.get('password');
    if (!email) return fail(400, { email, missing: true });
    const user = await db.getUser(email);
    if (!user || user.password !== db.hash(password)) {
      return fail(400, { email, incorrect: true });
    }
    cookies.set('sessionid', await db.createSession(user), { path: '/' });
    return { success: true };
  }
};

Anatomy: action receives RequestEvent, can read request.formData(), can write cookies, can return / fail / redirect / error. The return value goes into the form prop, onlyfor this responsevalid (disappears on reload).Safe for repopulation— Only echo fields the user permits,neverecho back passwords.

Redirects: same as load, use redirect(status, location)。

After Action load: after the action completes (unless redirect / unexpected error) the page re-renders—load the function re-runs.handle hook does notre-run—if in handle you read locals.userfrom a cookie, after the action modifies the cookie you mustmanually update event.locals。

Critical: use:enhance (progressive enhancement)

use:enhance is the action for <form> , giving the form the ability to "work without JS, and be smoother with JS".Onlyuse for method="POST" + +page.server.js actions; using it for +server.js or GET will error.

Minimal usage:

<script>
  import { enhance } from '$app/forms';
  /** @type {import('./$types').PageProps} */
  let { form } = $props();
</script>
<form method="POST" use:enhance>
  <input name="email" value={form?.email ?? ''}>
</form>

Default behavior: mimics the browser's native behavior but avoids full-page refresh—updates form / page.form / page.status(onlysame-page actions, otherwise it will not update), reset <form>, on success invalidateAll, on redirect goto, on error renders the most recent +error.svelte, resets focus.

Custom SubmitFunction(returning a callback overrides the default post-submit behavior; to restore it use update() or applyAction(result)):

<script>
  import { enhance, applyAction } from '$app/forms';
  import { goto } from '$app/navigation';
  let submitting = $state(false);

  /** @type {import('./$types').PageProps} */
  let { form } = $props();
</script>

<form
  method="POST"
  use:enhance={({ formElement, formData, action, cancel, submitter }) => {
    submitting = true;
    return async ({ result, update }) => {
      submitting = false;
      if (result.type === 'redirect') {
        goto(result.location, { invalidateAll: true });
      } else {
        await applyAction(result);  // 等价于默认 success/failure/redirect/error 行为
      }
    };
  }}
>
  <button disabled={submitting}>Save</button>
</form>

Fully handwritten(without use:enhance): use the submit event + fetch + deserialize(cannotuse JSON.parse, because the result contains Date / BigInt):

<script>
  import { invalidateAll, goto } from '$app/navigation';
  import { applyAction, deserialize } from '$app/forms';
  /** @param {SubmitEvent & { currentTarget: EventTarget & HTMLFormElement }} e */
  async function handleSubmit(e) {
    e.preventDefault();
    const data = new FormData(e.currentTarget, e.submitter);
    const response = await fetch(e.currentTarget.action, {
      method: 'POST', body: data,
      headers: { 'x-sveltekit-action': 'true' }  // 同名 +server.js 时强制走 action
    });
    const result = deserialize(await response.text());
    if (result.type === 'success') await invalidateAll();
    applyAction(result);
  }
</script>
<form method="POST" onsubmit={handleSubmit}>...</form>

When the same route has +server.js ,,fetch defaults to +server.js. To force POST to the action you must add the header x-sveltekit-action: true。

Critical: Page options (prerender/entries/ssr/csr/trailingSlash)

Page options control how the entire page (or a subtree) renders. Export from +page.js / +page.server.js / +layout.{js,server.js} .Child overrides parent—you can enable prerender in the root layout and disable it for individual pages.

// +page.js / +layout.js / +page.server.js
export const prerender = true;          // 构建时生成 HTML
export const prerender = false;         // 显式禁用(用于根 layout 开启全部 prerender 的场景)
export const prerender = 'auto';        // 可 prerender 也可 SSR(不写入 manifest 排除)
export const ssr = false;               // 仅 CSR——空 shell
export const csr = false;               // 不发任何 JS
export const trailingSlash = 'always' | 'never' | 'ignore';

// entries —— 动态路由告诉 prerender 哪些值
// src/routes/blog/[slug]/+page.server.js
/** @type {import('./$types').EntryGenerator} */
export function entries() {
  return [{ slug: 'hello-world' }, { slug: 'another-post' }];  // 可 async
}

// config —— adapter-specific
/** @type {import('some-adapter').Config} */
export const config = { runtime: 'edge' };
  • prerender: the content is the same for all users (marketing/docs/blog); not applicable to cookies/url.searchParams/user state/form action (POST requires a server). For dynamic routes use entries() or kit.prerender.entries. Error "marked as prerenderable, but were not prerendered" → add entries / link / change 'auto'
  • ssr = false: setting it in the root layout turns the whole app into an SPA.ssr = false + csr = false = render nothing,forbidden
  • csr = false:<script> is stripped out,<form> is unavailable use:enhance, links become native browser navigations, HMR stops working. During development csr = dev; keeps HMR
  • trailingSlash:'never'(default)/about/ → 301 → /about;'always' prerender output about/index.html;'ignore' not recommended (breaks SEO)
  • config objecttop-level merge(no deep merge)—child layouts / pages only override the keys that need changing

Quick Fixes

  • Private API exposes credentials → change to +page.server.js or a universal relay of non-sensitive fields
  • data prop undefined → missing let { data } = $props(); or type declaration
  • Streaming data hydration is lost → a universal load's Promise is not streamed, change to +page.server.js
  • During streaming setHeaders errors → headers cannot be changed after the response starts streaming
  • fail() then form fields are empty → the second parameter must include the echoed fields ({ email })+ value={form?.email ?? ''}
  • After the action changes the cookie the page still shows the old user → handle only runs once, you must manually event.locals.user = ...
  • use:enhance does not take effect → it must be method="POST" + POST to +page.server.js action
  • "marked as prerenderable, but not prerendered" → add entries() or kit.prerender.entries
  • entries() wrong location → must be ina leaf with dynamic parameters(+page.js/+page.server.js/+server.js), not layout
  • parent() waterfall → do not depend on parent() of getData(params) await first, then await parent()
  • url.hash undefined during SSR → no fragment on the server; change onMount read

Gotchas

  • Server load return values must be serializable:Map/Set/Date/BigInt/RegExp/circular references OK (devalue), butmust notreturn a Svelte component class / class instance (unless customized via transport hook)
  • load functions should be pure—do not share across requests at the top level of +page.server.js at the top level let user (multi-tenant long lifecycle, state will leak)
  • Dependency tracking only applies to the top-level body: accessing params.x inside a promise after await will not trigger a rerun (dev warning)
  • searchParams tracking granularity:get/getAll/has independent;url.searchParams accessing as a whole is equivalent to url.search tracking the entire string
  • form prop only responds to existence—cleared on refresh (only the receipt of this submission, not persistent data)
  • use:enhance does not update across pages by default form: when from /a POST /b?action , /a the form does not update—requires applyAction(result)
  • +server.js conflicts with +page.server.js of the same name:fetch('/x') defaults to +server.js; POST to an action must add x-sveltekit-action: true header
  • +page.js bridges server load: a missing layout.js is treated as ({ data }) => data, automatically passing server load data
  • streaming + redirect conflict: once the response starts streaming, headers cannot be changed; you cannot redirect
  • Lambda/Firebase buffer the entire page—does not hold streaming responses
  • trailingSlash: 'ignore' breaks SEO:/x and /x/ are different URLs
  • csr = false incompatible with HMR: during development csr = dev; temporarily enabled
  • redirect is caught in try {...} within—directly redirect() do not wrap in try
  • echo safety: do not echo passwords/tokens in form data, only echo allowed fields

FAQ

Q: +page.js vs +page.server.js which to choose? A: private credentials/DB/cookies → server; public API/non-serializable objects → universal. Both can exist at the same time, server runs first.

Q: page.data vs data prop? A: data prop = merge of the current component + all ancestors;page.data = data returned by the current page, readable from any ancestor.

Q: How to force load to rerun? A: invalidate(url) precise invalidation (by URL or depends tag), or invalidateAll() full invalidation. In server load, fetch(url) does not automatically depend on the URL, must depends(url)。

Q: After form submission, form prop is gone? A: Normal.form prop exists only while the response exists; it is cleared on refresh. Persistent data should go into the DB and then be read by load .

Q: use:enhance + redirect? A: By default it calls goto(result.location). For customization, check in the callback result.type === 'redirect' then goto(result.location, { invalidateAll: true })。

Q: prerender /blog/[slug] dynamic route? A: Add entries() The function returns a list of slugs, or in svelte.config.js in kit.prerender.entries config.

Q: Turn the entire app into an SPA? A: Root +layout.js set export const ssr = false;(SSG not recommended).

Q: Disable JS? A: export const csr = false;—no hydration,<form> still works (POST full-page refresh).

Q: Submit the same form to different actions? A: <button formaction="?/other"> override <form action>。

Q: Is action data type-safe? A: /** @satisfies {import('./$types').Actions} */ annotation;form prop uses ActionData type.

Q: Can streaming be used on serverless? A: Lambda/Firebase buffer the entire page; NGINX needs to be configured not to buffer.

Q: parent() Synchronous or asynchronous? A: Must await parent()。

Examples & References tables

Key API Overview

Concept Key Files / APIs
Universal load +page.js / +layout.js PageLoad / LayoutLoad
Server load +page.server.js / +layout.server.js PageServerLoad / LayoutServerLoad
Read data across components $app/state of page / App.PageData
URL data params / route.id / url(hash SSR unavailable)
Cookies / Headers cookies / setHeaders(server only)
Errors/redirects error() / redirect() / goto
Streaming server load returns a Promise that is not awaited
Dependency tracking depends / untrack / invalidate / invalidateAll
Form actions actions / fail / redirect / form prop
Progressive enhancement enhance / applyAction / deserialize
Page options prerender / entries / ssr / csr / trailingSlash / config

File Index

File Topic Number of examples
examples/load-functions.md Full spectrum of load functions 17
examples/form-actions.md form action + use:enhance 17
examples/page-options.md Full spectrum of page options 12
File Topic
references/load-functions-reference.md load Complete API / parameters / return values / $types
references/form-actions-reference.md action API / use:enhance callbacks / hook integration
references/page-options-reference.md All page options / constraints / behavior
references/rerunning-loads-reference.md When to rerun / manual invalidate / untrack