Svelte journey | SvelteKit: Introduction, Routing, Data Loading, Shared Modules

chillyhill

Denys Sych

Posted on January 5, 2024

Svelte journey | SvelteKit: Introduction, Routing, Data Loading, Shared Modules

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:
  • Default folder structure:
    • /src
      • /app.html — page template file;
      • /app.d.tsapp-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

Enter fullscreen mode Exit fullscreen mode
// src/routes/+layout.svelte
<nav>
    <a href="/">home</a>
    <a href="/about">about</a>
</nav>

<slot /> // mount node for / and /about pages

Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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' };
}

Enter fullscreen mode Exit fullscreen mode

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:

  1. Create src/params/<checker-name>.js.
  2. 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]

Enter fullscreen mode Exit fullscreen mode
  • 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 the root 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 put export 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>

Enter fullscreen mode Exit fullscreen mode

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 };
}

Enter fullscreen mode Exit fullscreen mode
  • The universal load function can get data from the parent server load 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() };
}

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode
  • 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 all load functions.
    • invalidateAll reruns load functions without any URL dependencies, which invalidate(() => 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'
    });
}

Enter fullscreen mode Exit fullscreen mode

Cookies

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'
    };
}

Enter fullscreen mode Exit fullscreen mode
  • cookies.set(name, ...) causes a Set-Cookie header to be written and updates the internal state of cookies (using the cookie 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>

Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
chillyhill
Denys Sych

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