SvelteKit Cheat Sheet
Jul 29, 2023
9 min read
SvelteKit is a powerful framework for building web applications that brings together the simplicity of Svelte's component-based approach with advanced features for routing, server rendering, and more. This cheat sheet captures key concepts and techniques from a SvelteKit tutorial, helping developers quickly reference and apply their knowledge:
File Structure
server side+hook.server.svelte
+layout.svelte
share common js using </slot>
+layout.server.js
load data for every child route+server.js
api only (and don't need to have +page.svelte)
client side+page.server.js
load data and handle api request+page.js
: dynamic component+page.svelte
content
Setting cookies
jsexport function load({ cookies }) { const visited = cookies.get('visited'); cookies.set('visited', 'true', { path: '/' }); return { visited };}
Props
js<script> export let data;</script><h1>Hello {data.visited ? 'friend' : 'stranger'}!</h1>
Shared module
src/lib
- use $lib to access module instead of ../../
Form
Default form
input from user'
js<form method="POST"> <label> add a todo: <input name="description" autocomplete="off" /> </label></form>
jsexport const actions = { default: async ({ cookies, request }) => { const data = await request.formData(); db.createTodo(cookies.get('userid'), data.get('description')); }};
Named form actions
jsexport const actions = { create: async ({ cookies, request }) => { const data = await request.formData(); db.createTodo(cookies.get('userid'), data.get('description')); }, delete: async ({ cookies, request }) => { const data = await request.formData(); db.deleteTodo(cookies.get('userid'), data.get('id')); }};
<form method="POST" action="?/create">
was defined on another page, you might have something like /todos?/create. Since the action is on this page, we can omit the pathname altogether, hence the leading ?
character.<form method="POST" action="?/delete">
Notice that we haven't had to write any fetch code or anything like that βdata updates automatically.
Validation and Throw Error
server side
js// server sideif (todos.find((todo) => todo.description === description)) { throw new Error('todos must be unique');}import { fail } from '@sveltejs/kit';try { db.createTodo(cookies.get('userid'), data.get('description')); } catch (error) { return fail(422, { description: data.get('description'), error: error.message }); }
client, access the returned value via the form
prop
js<script> export let data; export let form;</script><h1>todos</h1>{#if form?.error} <p class="error">{form.error}</p>
Progressive Enhancement
- run js code when submitting a form, useful when a page have many actions within the same url
- enhance the element behaviors using JS without reloading when updating the page
jsimport { enhance } from '$app/forms';// add the use:enhance directive to the <form> elements:<form method="POST" action="?/create" use:enhance={func}>
SubmitFunction
jsfunction enhance< Success extends Record<string, unknown> | undefined, Failure extends Record<string, unknown> | undefined>( form_element: HTMLFormElement, submit?: import('@sveltejs/kit').SubmitFunction<Success, Failure>): { destroy(): void;};
js<form method="POST" use:enhance={({ formElement, formData, action, cancel, submitter }) => { // `formElement` is this `<form>` element // `formData` is its `FormData` object that's about to be submitted // `action` is the URL to which the form is posted // calling `cancel()` will prevent the submission // `submitter` is the `HTMLElement` that caused the form to be submitted // callback return async ({ result, update }) => { // `result` is an `ActionResult` object await update({reset: true}); // the default logic that would be triggered if this callback wasn't set }; }}>
+server.js
- use +server.js files to expose (for example) a JSON API
js// +page.svelte<script> function rerun() { fetch('/api/ci', { method: 'POST' }); }</script><button on:click={rerun}>Rerun CI</button>
js// +server.js/** @type {import('./$types').RequestHandler} */export function POST() { // do something}
example
js// src/routes/todo/[id]/+server.jsimport * as database from '$lib/server/database.js';export async function PUT({ params, request, cookies }) { const { done } = await request.json(); const userid = cookies.get('userid'); await database.toggleTodo({ userid, id: params.id, done }); return new Response(null, { status: 204 });}export async function DELETE({ params, cookies }) { const userid = cookies.get('userid'); await database.deleteTodo({ userid, id: params.id }); return new Response(null, { status: 204 });}
js// src/routes/+page.svelteawait fetch(`/todo/${todo.id}`, { method: 'PUT', // <------------------- body: JSON.stringify({ done }), headers: { 'Content-Type': 'application/json' } });
Built-in Stores
- access route
jsimport { page, navigating, updated } from '$app/stores';// $page.url.pathname// $navigating.to.url.pathname
- handling page version
js{#if $updated} <p class="toast"> A new version of the app is available <button on:click={() => location.reload()}> reload the page </button> </p>{/if}
Error Handling
Throwing error
error
vsError
- Expected error (no log and stack trace)
jsimport { error } from '@sveltejs/kit';throw error(420, 'enhance your calm');
- Unexpected error (a bug in the app, (have log and stack trace))
jsthrow new Error('Kaboom!');
error page
+error.svelte
Redirect
jsimport { redirect } from '@sveltejs/kit';export function load() { throw redirect(307, '/b');}// 303 β for form actions, following a successful submission// 307 β for temporary redirects// 308 β for permanent redirectsexport function handleLoginRedirect( event, message = "You must be logged in to access this page") { const redirectTo = event.url.pathname + event.url.search return `/login?redirectTo=${redirectTo}&message=${message}`}export const load = async (event) => { if (!event.locals.user) { throw redirect(302, handleLoginRedirect(event)) }}
Goto in client side
<script> import { FilePlus } from 'lucide-svelte'; import { Button } from '$components/ui/button'; import { goto } from '$app/navigation'; function create_post() { goto('/blog/create'); } export let data;</script> <svelte:head> <title>Blog</title> <meta name="description" content="About this app" /></svelte:head> <div class="flex flex-row space-x-4"> <div class="basis-1/4 items-center justify-center flex flex-col space-y-4"> <div> <Button class="w-max" on:click={create_post}> <FilePlus class="mr-2 h-4 w-4" /> New </Button> </div> <div>tags</div> </div> <div class="grow items-center justify-center">02</div></div>
Hooks
- middleware
- intercept and override the framework's default behaviour.
Intercepting pages
js//hooks.server.jsexport async function handle({ event, resolve }) { // do something in event // access the page.server.js let response = await resolve(event); // do something in response return response;}
Intercepting fetches
js// it can be used to make credentialed requests on the serverexport async function handleFetch({ event, request, fetch }) { const url = new URL(request.url); if (url.pathname === '/a') { return await fetch('/b'); } return await fetch(request);}export async function load({ fetch }) { const response = await fetch('/a'); // <-- intercepted fetch return { message: await response.text() };}
Page options
ssr
β whether or not pages should be server-renderedcsr
β whether to load the SvelteKit clientprerender
β whether to prerender pages at build time, instead of per-requesttrailingSlash
β whether to strip, add, or ignore trailing slashes in URLs
+page.server.js
export const ssr = false;
export const csr = false;
This means that no JavaScript is served to the clientexport const prerender = true;
The advantage is that serving static data is extremely cheap and performant, allowing you to easily serve large numbers of users without worrying about cache-control headers (which are easy to get wrong).
Setting prerender to true inside your root +layout.server.js
effectively turns SvelteKit into a static site generator (SSG).
Link options
Preloading
html<a href="/slow-a" data-sveltekit-preload-data>slow-a</a>
SvelteKit will begin the navigation as soon as the user hovers over the link (on desktop) or taps it (on mobile)
"eager" β preload everything on the page following a navigation
"viewport" β preload everything as it appears in the viewport
"hover" (default) as above
"tap" β as above
"off" β as above
Preloading programmatically
jsimport { preloadCode, preloadData } from '$app/navigation';// preload the code and data needed to navigate to /foopreloadData('/foo');// preload the code needed to navigate to /bar, but not the datapreloadCode('/bar');
Reloading
SvelteKit is holding the page Snapshots even switch pages to disable this behaviour. You can do so by adding the data-sveltekit-reload attribute on an individual link
html<nav data-sveltekit-reload> <a href="/">home</a> <a href="/about">about</a></nav>
Routing
Optional route parameters
src/routes/[[lang]]/+page.server.js
jsconst greetings = { en: 'hello!', de: 'hallo!', fr: 'bonjour!'};export function load({ params }) { return { greeting: greetings[params.lang ?? 'en'] };}
Route regular expression match
src/routes/colors/[color=hex]
js// src/params/hex.jsexport function match(value) { return /^[0-9a-f]{6}$/.test(value);}
Routing group
some routes need auth, add the subfolder for pages under (authed)
js// src/routes/(authed)/+layout.server.jsimport { redirect } from '@sveltejs/kit';export function load({ cookies, url }) { if (!cookies.get('logged_in')) { throw redirect(303, `/login?redirectTo=${url.pathname}`); }}// src/routes/(authed)/+layout.svelte<form method="POST" action="/logout"> <button>Log out</button></form>// src/routes/login/+page.server.jsexport const actions = { default: ({ cookies, url }) => { cookies.set('logged_in', 'true', { path: '/' }); throw redirect(303, url.searchParams.get('redirectTo') ?? '/'); }};// src/routes/logout/+page.server.jsexport const actions = {default: ({ cookies }) => { cookies.delete('logged_in', { path: '/' }); throw redirect(303, '/'); }};
Layout breaksrc/routes/a/b/c/+page.svelte
inherits four layouts:
src/routes/+layout.svelte
src/routes/a/+layout.svelte
src/routes/a/b/+layout.svelte
src/routes/a/b/c/+layout.svelte
rename `+page@[level].svelte` would put the page inside routes/.../[level]** The root layout applies to every page of your app, you cannot break out of it.
Data loading+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
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 (not instance).
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.
js// src/routes/+page.server.jsexport async function load() { return { message: 'this data came from the server', cool: false };}// src/routes/+page.jsexport async function load({ data }) { const module = data.cool ? await import('./CoolComponent.svelte') : await import('./BoringComponent.svelte'); return { component: module.default, message: data.message };}
js// load dynamic component<script> export let data;</script><svelte:component this={data.component} message={data.message} />
Using parent data
// parent: src/routes/+layout.server.jsexport function load() { return { a: 1 };}// child: src/routes/sum/+layout.jsexport async function load({ parent }) { const { a } = await parent(); return { b: a + 1 };}// child page: src/routes/sum/+page.jsexport async function load({ parent }) { const { a, b } = await parent(); return { c: a + b };}<script> export let data;</script><p>{data.a} + {data.b} = {data.c}</p><p><a href="/">home</a></p>
Using child data
// a parent layout might need to access page data or data from a child layout<script> import { page } from '$app/stores';</script><svelte:head> <title>{$page.data.title}</title></svelte:head>
Invalidate /reload route (e.g. api)
// with the same URL, Kit only run once for optimization// but the data may change over timeonMount(() => { const interval = setInterval(() => { invalidate('/api/now'); // <---route }, 1000); return () => { clearInterval(interval); };});// invalidate(...)// takes a URL and re-runs any load functions that depend on it.
Manual invalidation/dependency
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() };}<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>
When do load functions re-run?
To summarize, a load function will re-run in the following situations:
It references a property of params whose value has changedIt references a property of url (such as url.pathname or url.search) whose value has changed. Properties in request.url are not trackedIt calls await parent() and a parent load function re-ranIt declared a dependency on a specific URL via fetch (universal load only) or depends, and that URL was marked invalid with invalidate(url)All active load functions were forcibly re-run with invalidateAll()params and url can change in response to a <a href=".."> link click, a <form> interaction, a goto invocation, or a redirect.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.
Environment variables
$env/dynamic/private
$env/static/private
$env/static/public$env/dynamic/publicStatic vs dynamicThe static in $env/static/private indicates that these values are known at build time, and can be statically replaced.Static variables get replaced at build time and dynamic variables get replaced at runtime. Static variables allow compile-time computations which can give better performance - e.g. if they're used in an if condition which contains expensive code inside such as a dynamic import. But for the most part the question is really just about when you want to set the variable - buildtime or runtime.
Locals
The interface that defines event.locals, which can be accessed in hooks (handle, and handleError), server-only load functions, and +server
.js
files.
interface Locals {}
jsimport { afterNavigate } from '$app/navigation'// if we came from /posts, we will use history to go back to preservelet canGoBack = false afterNavigate(({ from }) => { if (from && from.url.pathname.startsWith('/posts')) { canGoBack = true } }) function goBack() { if (canGoBack) { history.back() } }