Svelte journey | SvelteKit: Introduction, Routing, Data Loading, Shared Modules
Denys Sych
Posted on January 5, 2024
Welcome,
This article marks the beginning of our exploration of @sveltejs/kit
! SvelteKit is a metaframework that contains the core toolset needed to build a variety of applications. In this article, we embark on a journey through key aspects: Routing, Data Loading, and Shared Modules, unveiling the power and simplicity that SvelteKit brings to web development.
General Information
SvelteKit, a robust framework, offers a seamless transition from Server-Side Rendering (SSR) to a Single Page Application (SPA) after the initial load. Default configuration files, such as svelte.config.js
and vite.config.js
, along with a well-defined folder structure in /src
, provide a solid foundation for building modern web applications. The /routes
directory, following filesystem-based routing, makes navigation intuitive and efficient.
- SSR by default → improved load performance, SEO-friendly:
- Transitions to SPA after the initial load;
- Still, this can be configured if needed.
- Default configuration files in the root folder:
- svelte.config.js;
- vite.config.js;
-
package.json
,.prettierrc
, etc.
- Default folder structure:
-
/src
-
/app.html
— page template file; -
/app.d.ts
— app-wide interfaces (for TypeScript); -
/routes
— the routes of the app; -
/lib
— files that can be imported via$lib
alias in this folder;
-
-
/static
— for any assets used in the app.
-
Routing
SvelteKit's routing system is structured, with each +page.svelte
file inside src/routes
creating a corresponding page in the app. This approach simplifies route structuring and allows for easy parameterization using square brackets in the file path.
SvelteKit uses filesystem-based routing — the route path is the same as the folder path.
Route structuring | +page.svelte
Every +page.svelte
file inside src/routes
creates a page in the app.
- e.g.
src/routes/+page.svelte
→/
,src/routes/about/+page.svelte
→/about
Common layout | +layout.svelte
To have a common layout for some of the routes, you need to create a +layout.svelte
file in the topmost folder that shares this layout. You need to specify the common layout here and put the <slot />
element to indicate where pages should mount.
src/routes/
├ about/
│ └ +page.svelte
├ +layout.svelte
└ +page.svelte
// src/routes/+layout.svelte
<nav>
<a href="/">home</a>
<a href="/about">about</a>
</nav>
<slot /> // mount node for / and /about pages
Route parameters
The file path should contain a folder with a name in square brackets — its name is the parameter’s name.
src/routes/blog/[slug]/+page.svelte // single dynamic parameter
src/routes/blog/[bar]x[baz]/+page.svelte // multiple dynamic parameters (allowed with some static literal as a separator)
Example:
// src/routes/+page.svelte
<a href="/param1xparam2">Navigate</a>
// src/routes/[bar]x[baz]/+page.svelte
<script>
import { page } from '$app/stores';
let bar = $page.params.bar;
let baz = $page.params.baz;
</script>
bar: {bar} | baz: {baz} // bar: param1 | baz: param2
Optional parameters
For example, you can have the locale as /fr/...
, but you can also have a default locale that is not represented in the path. In that case, the parameter should be in double square brackets:
// src/routes/[[lang]]/+page.server.js
export function load({ params }) {
return { lang: params.lang ?? 'en' };
}
Rest (any number of unknown) parameters
To match an unknown number of path segments, use a [...rest]
parameter. This is useful to catch unhandled routes, similar to the *
route definition in other routers.
Rest parameters do not need to go at the end — a route like /items/[...path]/edit
or /items/[...path].json
is totally valid.
Param matchers — validate route parameter
Param matchers allow you to validate parameters. For example, if the user ID should be numeric only, you can do the following:
- Create
src/params/<checker-name>.js
. - Define the checker for a route param
src/routes/<route>/[param=<checker-name>]
.
// src/params/id-matcher.js
export const match = (value) => /^[0-9]+$/.test(value);
// src/routes/user/[id=id-matcher]
- In case of a failed match, a
404
error is returned. - Matchers run both on the server and the client side.
Route groups — subset of routes without changing a route’s path
Layouts are a way to share UI and data loading logic between different routes. If we need to use a layout without sharing data loading logic and without keeping the folder-to-path structure, we use route groups.
For example, our /app
and /account
routes are behind authentication, while /about
should be available without that one. This allows us to omit routes like /authed/app
and keep just /app
.
- Create a folder (group) with a name inside brackets, e.g.,
src/routes/(authed)/+layout.server.js
Unlike normal directories, route groups do not affect the URL pathname of the routes inside.
Breaking out of layouts — independent children layout
Layouts are inherited by default. If you need to break this and have an independent layout:
- To break out, you need to add
@
to the page naming:-
page@.svelte
will reset all the hierarchy (except theroot
layout). -
page@<route-path-folder-name>.svelte
will reset to the<route-path-folder-name>
layout. -
+layout.svelte
in the root cannot be broken out.
-
Data loading
Efficient data loading is a cornerstone of SvelteKit. The load
function, available in various files like +page.js
, +layout.svelte
, and their server-side counterparts, empowers developers to fetch data dynamically. This data can then be seamlessly consumed in the corresponding Svelte files, providing a smooth integration of dynamic content.
- The
load({ url, route, params })
function in+page.js
,+page.server.js
,+layout.server.js
,+layout.js
allows you to fetch data. - It should then be consumed in the related Svelte file via
<script> export let data; </script>
. -
$page.data
is an alternative way to access the page's data or if you need to access the page's data in the layout or in children. The+page.svelte
component, and each+layout.svelte
component above it, has access to its own data plus all the data from its parents:
<script> import { page } from '$app/stores'; </script> <svelte:head> <title>{$page.data.title}</title> </svelte:head>
Files without the suffix
.server.
run on both the client and server (unless you putexport const ssr = false
inside), while files with this suffix run on the server only (you can interact with DBs, use secrets here, etc.). More details on that: universal vs server. It is also covered further in the Universal load functions section.
Page data | +page.server.js
- The
+page.server.js
file should be placed next to the target+page.svelte
. This file runs only on the server, including client-side navigations. - The
+page.server.js
file itself can import other non-Svelte files to fetch data. - If you need to handle errors (e.g., incorrect route path), you can throw an
error
from@sveltejs/kit
.
// src/routes/blog/mocked-data.js (more realistic: import * as posts from '$lib/server/posts.service.js';
export const posts = [
{
slug: 'welcome',
title: 'Welcome!',
content:'<p>Nice that you are here</p>'
}
];
// src/routes/blog/[slug]/+page.server.js
import { posts } from '../mocked-data.js'; // get data with some service
import { error } from '@sveltejs/kit';
export function load({ params }) { // function name must be load
const post = posts.find((post) => post.slug === params.slug);
if (!post) throw error(404, 'no post'); // throw an error if not found
return { post };
}
// src/routes/blog/[slug]/+page.svelte
<script>
export let data; // access point to exported values from +page.server.js
</script>
<h1>blog post</h1>
<h1>{data.post.title}</h1>
<div>{@html data.post.content}</div>
Layout data | +layout.server.js
We can load data not only for some page but also for a child route under the same layout. The mechanism is the same as for page data.
From navigation to navigation, it is usually needed to refresh the data. Invalidation should help there.
Universal load functions — when data does not come directly from your server
Load from +page.server.js
and +layout.server.js
is a good use-case when getting data directly from a database or reading cookies, dealing with secrets. When you deal with some data loading during client-side navigation, you may simply not need to hit your server directly but rather some other source. For example:
- You need to access some external, 3rd party API.
- Use in-memory data if some data persists here, and only go to the server if no in-memory data is available.
- You need to return something non-serializable from the
load
function, such as a referenced JS object, component, store, etc.
To have such behavior, your load
functions should not be in +page.js
or +layout.js
(no .server.
suffix). With that:
- Functions will run on the server during server-side rendering.
- They will also run in the browser when the app hydrates or the user performs a client-side navigation.
Server load
and universal load
together = server → universal → page data
When using both server load
and universal load
, the server load
return value is not passed directly to the page, but to the universal load
function as the data
property.
Access parent’s load
result inside own load
function
+page.svelte
and +layout.svelte
components have access to everything returned from their parent load
functions. But how to access the parent's data right in the load
function? We just need to use the parent
prop as follows:
// src/routes/+layout.server.js
export const load = () => ({ a: 1 });
// src/routes/sum/+layout.js
export async function load({ parent }) {
const { a } = await parent();
return { b: a + 1 };
}
// src/routes/sum/+page.js
export async function load({ parent }) {
const { c } = await fetchCFromThirdParty();
const { a, b } = await parent();
return { d: a + b + c };
}
- The universal
load
function can get data from the parent serverload
function, but not vice versa.
load
dependencies
Calling fetch(url)
inside a load
function registers url
as a dependency. Sometimes we can get data not through fetch
but generate it, get it from internal storage, etc. In that case, we need to define this dependency manually by calling depends
:
// src/routes/+layout.js
export async function load({ depends }) {
depends('data:now');
return { now: Date.now() };
}
Invalidation
SvelteKit has a built-in invalidation mechanism (e.g., it reruns when path params are changed; this mechanism can be disabled). However, in some cases, you need to force it and handle it manually, for example, when you need to invalidate data from the parent's load function. This can be done using the invalidate
function, which takes a URL and reruns any load
functions that depend on it:
// src/routes/+page.svelte
<script>
import { onMount } from 'svelte';
import { invalidate } from '$app/navigation';
export let data;
onMount(() => {
const interval = setInterval(() => {
invalidate('/dynamic-data-source'); // or 'dep:foo' in case of manual deps
}, 1000);
return () => {
clearInterval(interval);
};
});
</script>
- If you need to invalidate based on some pattern, you can pass a callback argument instead of a plain string:
invalidate(url => url.href.includes('foo'))
. -
invalidateAll()
can be used to rerun allload
functions.-
invalidateAll
rerunsload
functions without any URL dependencies, whichinvalidate(() => true)
does not.
-
Headers and Cookies
Inside a load
function (also in form actions, hooks, and API routes, which we'll learn about later), we have access to the setHeaders
function:
export function load({ setHeaders }) {
setHeaders({
'Content-Type': 'text/plain'
});
}
setHeaders
cannot be used with the Set-Cookie
header. Instead, use cookies
:
export function load({ cookies }) {
const visited = cookies.get('visited');
cookies.set('visited', 'true', { path: '/' }); // cookies.set(name, value, options)
return {
visited: visited === 'true'
};
}
-
cookies.set(name, ...)
causes aSet-Cookie
header to be written and updates the internal state of cookies (using thecookie
package under the hood). - It is strongly recommended to configure the
path
when setting a cookie since the default behavior of browsers is to set the cookie on the parent of the current path.
Shared Modules
If you need to share code across multiple places, the right place to keep it is the src/lib
folder. You can access it via the $lib
alias:
// src/lib/message.js
export const message = 'hello from $lib/message';
// src/routes/a/deeply/nested/route/+page.svelte
<script>
import { message } from '$lib/message.js';
</script>
<p>{message}</p>
That's all for now! I hope you've enjoyed and felt the real power and potential that is baked into SvelteKit. In the next chapter, we will overview the rest of the toolset, such as hooks, page and link tuning.
See you soon, take care, and go Svelte!
Resources
- SvelteKit tutorial | Introduction, Routing, Loading data, Headers and cookies, Shared modules
- SvelteKit tutorial | Advanced routing, Advanced Loading
- SvelteKit Docs | Advanced Layouts
- Svelte Docs | Loading data
Posted on January 5, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
January 5, 2024