iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🐈

Getting Started with Svelte and SvelteKit

に公開

Update (December 15, 2022)

SvelteKit v1 has been released! 🎉
https://svelte.dev/blog/announcing-sveltekit-1.0

Introduction

Hello, it's been a while since I've written an article on Zenn, so my hands are shaking.
Just kidding, I'm not (ZennZenn) nervous at all.
Recently, I rebuilt my personal blog using SvelteKit.

Note: This may serve strongly as a personal memo.

So, I will be summarizing Svelte and SvelteKit.

What is Svelte?

Svelte is a frontend framework that allows you to declare UI declaratively, similar to React or Vue.
However, as explained below, Svelte does not use a Virtual DOM (VDOM). This is because Svelte is a compiler.

Additionally, Svelte has an extremely small bundle size. Since it can also be applied partially, I think it is easy to use for small-scale applications. (Of course, it can be used for large applications as well.)

Features of Svelte

The beginning of the official documentation mentions the following three features.

Less Code to Write

The first feature is the reduction in the amount of code required to build an application.
Overall, in addition to the simple syntax, the amount of code seems to be reduced because it uses two-way binding (like the one in Vue) and state updates are performed mutably.
The documentation also mentions that the amount of code needed to achieve the same task is smaller.
Apparently, what takes 442 characters in React and 263 characters in Vue.js can be achieved in 145 characters with Svelte. Amazing.

If you just want to do a "Hello World", you can write it as follows:

<script>
	let name = 'world';
</script>

<h1>Hello {name}!</h1>

Also, a common counter app can be written with the following code:

<script>
	let count = 0;

        const  handleClick = () => {
		count += 1;
	}
</script>

<button on:click={handleClick}>
	Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

It's very simple.

Every source code contains bugs. One way to reduce the possibility of bugs is to decrease the amount of source code. Svelte achieves this.

No Virtual DOM

A major difference compared to React and Vue is that Svelte does not use a VDOM.
Also, Svelte's position is that of a compiler.
Instead of executing the VDOM at runtime like React or Vue, code written in Svelte is compiled and output as vanilla JS and CSS files.

As you would know if you have ever built your own VDOM library, the code required to build a VDOM tree, reflect it in the real DOM, and compare values using variables like oldNode and newNode to update differences whenever there is a change will inevitably become large.

Furthermore, in Virtual DOM is pure overhead, it says, "Let's retire the 'virtual DOM is fast' myth once and for all." I read that and thought, "Indeed."

Moreover, the compiled code only includes necessary processing (it performs tree shaking and even trims down the CSS), so there is no redundant logic, and it runs at high speed because the bundle size is small.

Truly Reactive

Svelte allows you to write reactive code. The code generated by Svelte is code that detects state changes and reflects them directly in the DOM.
That reactive code is expressed through assignment.

Therefore, when updating a value, it is expressed as:

count += 1;

And in the compiled code, the process where the value is changed is expressed as:

count += 1; // Assignment becomes the trigger
$$invalidate('count', count); 

this is how it is represented.

How to Write

As mentioned briefly above, various things can be embedded in the template.

Basically, it can be written in the following format.
If you want to know more, please refer to the documentation.

<script>
	// JS/TS logic
</script>

<script context="module">
	// Executed once when the module is first evaluated, rather than for each component instance
	// Can access the script tag, but not vice versa
	// Default export is not possible
</script>

<style>
	/* Styles such as CSS (Sass) */
</style>

<div>
	<!-- Markup -->
</div>

script

You can write logic in the script tag section.
In Svelte, you can use export to declare variables as properties (props).

<script>
	export let name;
	name = 'takurinton';
</script>

Note that exporting const, class, or function makes them read-only from outside the component.

<script>
	// Read-only
	export const thisIs = 'readonly';

	// Read-only
	export function greet(name) {
		console.log(`hello ${name}!`);
	}

	// This is updatable
	export let format = n => n.toFixed(2);
</script>

If you implement a counter, it looks like this. Pretty standard.

<script>
	let count = 0;
	function handleClick () {
		// Update the variable 'count' from the markup side
		count = count + 1;
	}
</script>

You can make things reactive by using JavaScript's label syntax. This runs whenever the dependent values change, just before the component updates.
Since it only targets dependent values, in this case, Total is updated only when x is updated.

<script>
	let x = 0;
	let y = 0;

	function yPlusAValue(value) {
		return value + y;
	}

	$: total = yPlusAValue(x); // Updated only when this changes; since y is not here, it doesn't interfere
</script>

Total: {total}
<button on:click={() => x++}>
	Increment X
</button>

<button on:click={() => y++}>
	Increment Y
</button>

Stores are also available. A store is a constructor that allows references to reactive objects.
If you want to reference a store, you can access its value within a component by prefixing it with $.
Additionally, you can register a value using .set. It can also be updated using assignment. However, this variable must be writable.

<script>
import { writable } from 'svelte/store';

const count = writable(0);
console.log($count); // 0

count.set(1);
console.log($count); // 1

$count = 2;
console.log($count); // 2
</script>

style

style tags are scoped to each component.
Upon compilation, a hash value for each component is applied in addition to the class names, allowing for unique identification.
Additionally, if you want to apply styles globally, use :global(property).
To maintain keyframes globally, use the -global- prefix at the start.

<style>
/* Applied only to p tags within this component */
p {
   color: pink;
}
   
/* Applied globally */
:global(body) {
   margin: 0;
}
   
/* Becomes global by adding -global- */
@keyframes -global-animation {
   from {
   	transform: translateX(0%);
   }
   to {
   	transform: translateX(100%);
   }
}
</style>

template

You can write markup in the template.
It is the same as HTML, but there are many convenient writing styles unique to Svelte.

Basic parts

First, external components can be used as usual.

<script>
	import MyComponent from './MyComponent.svelte';
</script>

<div>
	<MyComponent/>
</div>

You can also add attributes.
Default attributes function exactly the same as in HTML.

<!-- Can be used normally -->
<div class="hoge">
	<button disabled>It's disabled, so you can't click it</button>
</div>

Attributes can include JS syntax.

<a href="post/{slug}">post: {slug}</a>
<button disabled={!clickable}>...</button>

Arguments can also be passed to components. There are several ways to do this.

<!-- Standard pattern -->
<MyComponent foo={bar} answer={42} text="hello"/>

<!-- Passing an object; this can be done just like other frameworks -->
<MyComponent {...items}/>

<!-- Prefixing with $$ references all props passed to the component -->
<!-- Includes those not exported -->
<!-- Deprecated due to optimization issues -->
<MyComponent {...$$props}/>

<!-- restProps contains only the props not declared with export -->
<!-- This is also deprecated -->
<MyComponent {...$$restProps}/>

You can also perform operations within the template.

<p>{x} + {y} = {x + y}</p>

Conditionals and Loops

You can also handle logic such as conditional branching and loops.
An if statement can be expressed as {#if expression} {/if}, and else if and else can also be represented.

<!-- Conditional branching -->
{#if temperature > 30}
	<p>It's hot</p>
{:else}
	<p>It's okay</p>
{/if}

Loops use each. The syntax is similar to if, and it can be expressed as {#each expression as alias} {/each}.

<h1>user list</h1>
<ul>
	{#each users as user}
		<li>{user.name}: {user.age}</li>
	{/each}
</ul>

You can also use syntaxes like:

{#each expression as name, index}...{/each}
{#each expression as name (key)}...{/each}
{#each expression as name, index (key)}...{/each}
{#each expression as name}...{:else}...{/each}

Handling Promises

It is also possible to represent the state of a Promise using await.

{#await promise}
	<p>pending</p>
{:then value}
	<!-- resolve -->
	<p>The value is {value}</p>
{:catch error}
	<!-- rejected -->
	<p>rejected: {error.message}</p>
{/await}

If you don't care about each specific state, you can omit them.
For example, if you don't need to render anything during the pending state, you can write it like this:

{#await promise then value}
	<p>value: {value}</p>
{/await}

key

You can create a key to give an object a unique value. I feel like building your own VDOM helps you understand the importance of this area.

{#key value}
	<div transition:fade>{value}</div>
{/key}

Special Tags (Decoration)

Wrapping content in {@html content} allows it to be recognized as HTML.
For example, it can be used when converting markdown text of a blog post into HTML.

<div class="blog-post">
	<h1>{post.title}</h1>
	{@html post.content} // String converted from Markdown to HTML
</div>

Using {@debug expression} allows for debugging. While console.log() is often used in JS, using debug inside a Svelte template is convenient.

<script>
	let user = {
		name: 'takurinton',
		age: 21
	};
</script>

{@debug user}

<h1>Hello {user.name}!</h1>

Attributes

When handling events, use the on:eventname={handler} attribute.
The frequently appearing counter looks like this. It is expressed in a form like <button on:click={handleClick}>.
Also, it is possible to have multiple event handlers.

<script>
	let count = 0;
	function handleClick(event) {
		count += 1;
	}
</script>

<button on:click={handleClick}>
	count: {count}
</button>

Furthermore, performance is guaranteed not to be lost even when written inline, so for a simple counter like this, there is no problem with the following description. Is this the power of not using a VDOM? (Not that it changes much)

<button on:click="{() => count += 1}">
	count: {count}
</button>

bind:property={variable} can also be used. Normally, the flow of state goes from parent to child, but using bind allows you to flow it in reverse.

Managing the state of a select box looks like this.

<select bind:value={selected}>
	<option value={a}>a</option>
	<option value={b}>b</option>
	<option value={c}>c</option>
</select>

It seems there are many bindings for the video tag. I haven't tried them much, but the following can be used.

<video
	src={clip}
	bind:duration
	bind:buffered
	bind:played
	bind:seekable
	bind:seeking
	bind:ended
	bind:currentTime
	bind:playbackRate
	bind:paused
	bind:volume
	bind:muted
	bind:videoWidth
	bind:videoHeight
></video>

There are many other bindings, but I will omit them.

You can use class:name={value} when specifying styles.
For example, these two are equivalent.

<!-- Applies the 'active' class only when active is true -->
<div class="{active ? 'active' : ''}">...</div>
<div class:active={active}>...</div>

If you want to define a function that is called when an element is created, you can use use:action={parameters}.
Additionally, you can return an object using a destroy method, which will be called after the element is unmounted.

<script>
	function mount(node) {
		// Logic when mounted
		return {
			destroy() {
				// Logic when unmounted
			}
		};
	}
</script>

<div use:mount></div>

transition is triggered by elements entering or leaving the DOM as a result of a state change.

{#if visible}
	<div transition:fade>
		Fade in and fade out
	</div>
{/if}

Also, similar to transition, there is in:fn/out:fn.
When a block changes while a transition is in progress, in continues to run along with out.
However, if out is aborted, the transition will start over from the beginning.

{#if visible}
	<div in:fly out:fade>
		Flies in and fades out
	</div>
{/if}

If you want to express animations when an each block is rearranged, use animate:name={params}.
Animate must be on an immediate child element of a keyed block.

{#each items as item, index (item)}
	<li animate:flip>{item}</li>
{/each}

Components can have child content just like elements.
Content can define default values for child components using the <slot> element, which is provided when no child components are present.

<!-- MyComponent.svelte -->
<div>
	<slot>
		Displayed when no child components are rendered
	</slot>
</div>
<!-- Main.svelte -->
<MyComponent></MyComponent> <!-- If there are no child components, the slot content is displayed as is -->

<MyComponent>
	<p>If there are child components, they override the slot</p>
</MyComponent>

Tags

Using <svelte:self> allows a component to call itself recursively.
Note that since it is called recursively, you must use conditional branching to avoid infinite loops. Additionally, placing it at the top level is prohibited.

<script>
	export let count;
</script>

{#if count > 0}
	<p>Countdown: {count}</p>
	<svelte:self count="{count - 1}"/>
{:else}
	<p>Finished</p>
{/if}

Using <svelte:component> dynamically generates the component given to this. Additionally, when its state changes, it is destroyed and regenerated.

<svelte:component this={MyComponent} />

Using <svelte:window> allows you to remove event listeners when a component is destroyed, or to add event listeners to window without needing conditional branching to avoid window is not defined during SSR.

<script>
	function handleClick(event) {
		document.getElementById('takurinton');
	}
</script>

<!-- This can be used on the server side as well -->
<svelte:window on:click={handleClick}/>

<svelte:body> can do the same as <svelte:window>, but it must be used at the top level.

Using <svelte:head> allows you to represent the <head> tag.
This must also be used at the top level.
When performing SSR, this will be separate from the HTML generated on the server side. If you want to use it for OGP, etc., it needs to be generated on the server side.

<svelte:head>
	<link rel="stylesheet" href="./style.css">
</svelte:head>

Using <svelte:fragment> allows you to place content into a slot without wrapping it in a container DOM element.

<!-- MyComponent.svelte -->
<div>
	<slot name="header"></slot>
	<p>hogehoge</p>
	<slot name="main"></slot>
</div>
<!-- App.svelte -->
<MyComponent>
	h1 slot="header">Hello</h1>
	<svelte:fragment slot="main">
		<p>hello world</p>
	</svelte:fragment>
</MyComponent>

It got a bit long, but that's about it for Svelte. There are also APIs provided for the runtime, but since they overlap somewhat with the content explained below, I will omit them.

What is SvelteKit?

For Svelte, there is SvelteKit, which occupies a position similar to Next.js in the React ecosystem. It provides server-side rendering, various wrappers, and a rich ecosystem. Additionally, it uses Vite for bundling, which is extremely fast (though technically it's unbundled), providing a great developer experience. HMR (Hot Module Replacement) is a lifesaver.

To easily create a workspace, run the following command:

npm init svelte@next my-app

Basic Features

As mentioned above, since it occupies a position similar to Next.js, it provides comparable features. I will briefly outline the basic functions.

Routing

Following the Next.js style, files placed under routes/ are automatically routed to that path. You can use patterns like [id].svelte or [slug].svelte. One difference from the Next.js style is that files with extensions other than .svelte (such as JS or TS) are recognized as code that runs on the server side.

It looks something like routes/post/util.ts below.
route

My understanding is that these are used when you want to execute Node.js, so they might be close to what api/ is in Next.js. It feels like you can place them in any directory with high flexibility. Additionally, files or directories starting with an underscore are not included in routing.

It is also possible to include multiple dynamic parameters, such as routes/[category]/[slug].svelte. If the hierarchy of files or paths is unknown, you can match them using a syntax like ...slug. For example, representing a GitHub repository would look like /[org]/[repo]/tree/[branch]/[...file].

load Function

This is the equivalent of getInitialProps in Next.js. It is called before the component is initialized. In other words, if you are performing SSR (Server-Side Rendering), it runs at the SSR stage, and for subsequent navigations, it runs on the client side. The load function can take the following arguments:

<script context="module">
  export const load = async ({ page, fetch, session, context }) => {
    page; // Path, query parameters, etc.
    fetch; // A fetch function usable on both client and server
    session; // Used to store session data
    context; // Used to store context data
  };
</script>

The context="module" syntax appears here; this is a common expression in Svelte used when performing meta-level processing for the framework. When using context="module", meta-processing occurs, and when not using it, normal processing occurs, so you will often see two script tags in one file. One of the great things about this load function is that it can be used isomorphically. It feels similar to the advantage in Blitz where you can use server functions from the client via RPC transformation.

While blocking can be tedious, you can also run client-side processing separately, so that approach is also an option. Personally, I think this is a point to be happy about.

By the way, Svelte has a function called onMount, which is the equivalent of useEffect in React. If you use the load function, this becomes unnecessary, but you can still use it if you absolutely need to.

Shared Layouts

If you create a $layout.svelte file under a directory created for routing, it will be treated as a shared layout.
In Next.js terms, it's this part of the documentation

Also, you can represent error pages by creating an $error.svelte file. So convenient!

hooks.js

These are not "hooks" in the Next.js sense (well, those are actually React hooks), but by placing src/hooks.js, you can define code to be executed during SSR.

hooks provide two functions:

  • handle
    • Executed for every request on both the client and server sides and returns a response.
    • Generates a response based on the router and receives a resolve function.
  • getSession
    • Receives a request object and returns a session object accessible by the client.
    • Should contain information that is safe to expose to the user.
    • Executed every time a page is rendered.
    • session must contain serializable data and must not include functions or classes.

Defined Modules

SvelteKit has several special modules that are uniquely defined.

$app/env

Contains settings for the execution environment.
It seems to provide four return values:

  • amp
    • A boolean indicating whether AMP is included in the configuration.
  • browser
    • A boolean indicating whether the code is running on the browser or server side.
  • dev
    • A boolean indicating whether it is a development or production environment.
  • prerendering
    • A boolean indicating whether prerendering is being performed.

$app/navigation

A convenient module used for prefetching and page transitions.
It takes four return values:

  • goto(href, { replaceState, noscroll })
    • Returns a Promise that resolves when navigating to the specified href.
    • If replaceState is true, no new entry is created in the history.
    • noscroll prevents scrolling to the top of the page after navigation (stays at the current viewport).
  • invalidate(href)
    • Re-runs the load function of the current page if the resource in question was fetched.
    • Returns a Promise that resolves when the page is subsequently updated.
  • prefetch(href)
    • Performs a prefetch.
    • Returns a Promise that resolves when the prefetch is complete.
  • prefetchRoutes(routes)
    • Prefetches the code for routes that haven't been fetched yet.
    • Speeds up subsequent navigation.
    • Returns a Promise that resolves when the prefetch is complete.

$app/paths

I'm not exactly sure, but it seems to have two return values.
I can't quite grasp where to use them, but they exist (I guess).

  • base
    • Returns a string matching config.kit.paths.base (absolute path).
  • assets
    • Returns a string matching config.kit.paths.assets (relative path).

$app/stores

Stores depend on the context.
Stores are added to the context of the root component. This indicates that session and page information is not shared across all requests on the same server, but is unique to each request on the server.
$app/stores is a shortcut to each store in the context.
It provides four return values:

  • getStores
    • Returns { navigating, page, session } (the three below).
  • navigating
    • A readable store.
    • Holds values from the start to the end of navigation, becoming null when navigation finishes.
  • page
    • A readable store whose value reflects the object passed to the loading function.
    • Same values accessible in the load function (path, params, query).
  • session
    • A writable store.
    • Changes are not persisted on the server.
    • If you want to persist something, you need to implement it yourself.

$lib

Provides a shortcut to src/lib, or the directory specified as [config.kit.files.lib] in the config.
Normally, when components are nested, you have to access it like ../../../../lib/hoge.ts, but you can jump directly by specifying $lib.

$service-worker

Contains useful items that can be used within a service worker.
It takes three return values:

  • build
    • An array of strings representing files generated when building with Vite.
    • Suitable for caching via cache.addAll(build).
  • files
    • An array of strings representing files in the static directory or the directory specified by config.kit.files.assets.
  • timestamp
    • The result of calling Date.now() at the time of the build.
    • Can be used to generate unique cache names within the service worker to invalidate old caches upon deployment.

adapter

SvelteKit provides adapters so that a single codebase can work across multiple environments.

  • adapter-static
    • For performing SSG.
  • adapter-node
    • For creating a Node.js server to perform SSR.
  • adapter-xxx
    • The "xxx" part refers to external services like Vercel or Cloudflare Workers.
    • You can generate build files for each environment where Node.js runs (including serverless environments).

anchor options

In SvelteKit, you can add options to <a> tags.

sveltekit:prefetch

Adding prefetch will prefetch the linked page. This is similar to the Link component in Next.js.

3ca sveltekit:prefetch href="blog/what-is-sveltekit"4What is SvelteKit?3/a4

sveltekit:noscroll

When navigating to an internal link, the browser performs its default navigation behavior (resetting the scroll position to 0).
Adding this option disables that behavior and allows the transition to occur while maintaining the scroll position from before the transition.

3ca href="path" sveltekit:noscroll4Path3/a4

rel=external

SvelteKit recognizes <a> tags as internal links and automatically treats them as relative paths.

Therefore, if you want to link to an external site, you need to explicitly indicate it.

3ca rel="external" href="path"4Path3/a4

SSR

SvelteKit is designed to perform SSR by default, rendering components on the server, sending them to the client as HTML, and then performing hydration in the browser.

If you want to change this default configuration, you can control it on a per-app or per-page basis.
For per-page settings, you use context="module". This is used in page components rather than layout components. (If specified in both, the page takes precedence.)

Each setting can be controlled individually, but you cannot set both ssr and hydrate to false (which is common sense).

I haven't experimented much with this area myself, so I'm not entirely sure, but I've seen articles mentioning that the behavior can be a bit flaky, so it might be unstable.

ssr

If you set ssr to false, server-side rendering will be disabled, and SvelteKit will be treated as an SPA.
It seems SvelteKit recommends performing SSR.
You can change this based on the situation and your preference.

<script context="module">
	export const ssr = false;
</script>

router

SvelteKit includes client-side routing, which updates page content by taking over navigation (when a user clicks a link or goes forward/back) instead of letting the browser reload and handle navigation.
This can optionally be disabled (e.g., when you want to maintain the scroll position).

<script context="module">
	export const router = false;
</script>

hydrate

Similar to the ssr option, there is also a hydrate option, but at least one of them must be set to true.
A potential reason to choose not to perform hydration is if no JS is used at all on that page. In this case, displaying only the server-side generated HTML without hydration allows the page to be shown with fewer resources.

<script context="module">
	export const hydrate = false;
</script>

prerender

Just like pages that don't need hydration, if a page can be represented by simple HTML, it can be prerendered.
This starts from the app's root and generates HTML for any discoverable, prerenderable pages.

<script context="module">
	export const prerender = true;
</script>

AMP

You can express AMP settings with the following effects by using the config options:

  • You can represent the behavior when client-side JavaScript, including the router, is disabled.
  • The AMP boilerplate is inserted by using <style amp-custom>.
  • In development, requests are checked against the AMP validator, providing early warnings of errors.

Conclusion

I've summarized SvelteKit so far. I think these are the kinds of things that are good to know during development. I also thought it's great that you can configure things in detail, such as the config options.

However, while SvelteKit is nice, I also felt that it has some unstable aspects, such as having many bugs at this point (I personally ran into bugs with adapters and the load function and spent quite a bit of time on them).

Still, the small output size is attractive, and the approach of outputting vanilla JS instead of using a VDOM seems interesting, so please give it a try.
I'll be refactoring my blog from now on.

Note: I got a bit tired in the latter half and didn't really write about the "Next.js style" as much...
Note: The author is almost a complete beginner with Next.js.

Discussion