The newest framework for creating web apps with Svelte is here: SvelteKit. This framework is easy to use even for less experienced developers.
SvelteKit is the successor to Sapper, a compact yet powerful JavaScript framework powered by Svelte. The new release of SvelteKit is an upgrade to what Sapper provides and is currently in public beta.
Exploring SvelteKit myself left me impressed by the fact that it was quite easy to understand; it has fewer concepts to learn compared to other popular frameworks like React.
Let’s delve into the basics of Svelte and SvelteKit and eventually explore a SvelteKit example.
Svelte is a component library like React, and SvelteKit is the app framework like Next.js. While similar, the reason Svelte stands apart from React is because it provides a different way to think about web apps.
React uses virtual DOM diffing to decide the changes needed to update a UI, but Svelte is a compiler, which compiles your code and converts the Svelte components into JavaScript to render and update them, making it faster and lighter.
SvelteKit then does all the heavy lifting of setting up an app with server-side rendering, routing, and more, just like Next.js. However, SvelteKit also uses an adapter that can export your app to a specific platform and adapts well to serverless architecture. Since serverless architecture is becoming more prominent, it’s a good reason to try SvelteKit out.
You can use the official SvelteKit adapters for platforms like Netlify and Vercel.
By also providing features including server-side rendering, code splitting, and more, SvelteKit is especially useful for beginnings.
With that, let’s see how we can create a new project with SvelteKit.
Before we code an example app, we’ll play with the demo app that you get when you create a new project with SvelteKit and review some key concepts that will familiarize you with the framework.
Begin with inputting the following code snippet into a terminal. This will set up an app in the current directory.
npm init svelte@next
Then input the following to install all the dependencies and we’re good to go.
npm install
Also, if you’re using Visual Studio Code, install the official Svelte extension for syntax highlighting and other features for writing Svelte components, such as app pages.
SvelteKit sets up a routing system where files in your src/routes
determine the routes in your app. This directory can be changed by editing svelte.config.cjs
.
Note that src/routes/index.svelte
is the homepage.
By inputting npm run dev
, you start a development server. SvelteKit uses Vite behind the scenes making updates are blazing fast.
At this point, install the static adapter to build the pre-rendered version of the entire app by using the following:
npm i -D @sveltejs/adapter-static@next
Now, let’s explore some code, make some changes, and see the result.
We will add another route to the counter app that SvelteKit bootstrapped for us by inputting about.svelte
to the src/routes/
directory.
<!-- about page --> <svelte:head> <title>About</title> </svelte:head> <h1>About Page</h1> <p>This is the about page. Click <a href="/">here</a> to go to the index page.</p>
As you can probably guess, this will set up another route for us at /about
. To navigate to this page, we will add a link to the index page as well.
The index page already has the following line:
<p>Visit <a href="https://svelte.dev">svelte.dev</a> to learn how to build Svelte apps.</p>
We’ll just change it to the code below:
<p>Visit the <a href="/about">about</a> page</p>
When we click on the link, the internal router kicks in and handles the navigation. In fact, SvelteKit handles the navigation by default. The initial load is handled on the server side, then SvelteKit’s inbuilt router handles the subsequent navigation on the client side unless we specify otherwise.
SvelteKit allows you to disable this router by altering the Svelte configuration file svelte.config.cjs
. Setting the router
property to false disables the app-wide router. This will cause the app to send new requests for each page, meaning the navigation will be handled on the server side.
You can also disable the router on a per-page basis if needed. We’ll go ahead and see it in action by adding the following to the top of about.svelte
:
<script context="module" lang="ts"> export const router=false; </script>
I’ll talk about the context="module"
and lang="ts"
in a bit. For now, let’s run the app by npm run
dev
. We should expect all routing from the About page will be handled by the server, meaning when navigating from the About page, new requests to the server will be made. This is a fine little functionality SvelteKit provides us completely out of the box.
Looking at the script we were just working with, the scripts containing context="module"
are added directly to the module. This means they run once whenever the component is initialized as opposed to other scripts without context="module"
, which become a part of the instance — the component — and run whenever an instance is created and initialized.
So, variables in <script context="module"></script>
are shared among the instances of the default export of the module, which is the component itself.
The lang="ts"
tells the compiler that the language used is TypeScript. You need to use this if you chose TypeScript as the language during setup. If you’re using JavaScript, then there’s no need to do anything here.
As a little experiment, we’ll add this to the top of src/lib/Counter.svelte
:
<script context="module"> console.log("module code"); </script>
And then, add this line to the top of the already present instance-level script:
console.log("component code");
We’ll also include another counter component in index.svelte
by adding <Counter/>
.
So, what do we see when we run this? Since the two counters are independent of each other, the logs show that “module code” ran first, then the two “component code” messages appear.
Now, let’s add this to the bottom of about.svelte
:
<style> p { color:blue; max-width: 14rem; margin: 2rem auto; line-height: 1.35; } </style>
In Svelte, styles applied to components are scoped to the component. This style will only be applied to the About page.
You’ll also notice the $layout.svelte
component inside routes/
; this can display and style things that are persistent across different routes, like the footer, for example.
Let’s dive into how the layout component can wrap every component within itself, making it an ideal place to perform functions like providing the store and setting up the context.
First, let’s add this to the $layout.svelte
file:
<script> console.log("layout component"); </script>
Then add similar logging statements to the routes index.svelte
and about.svelte
. Start the development server, and look at the console in your browser; the layout message appears first and then the index message.
Now when we navigate to the About page, the logs show the added about component
line
As the $layout
component is rendered first, the pages are added and removed from the layout as they are needed by the router.
You can also use the lifecycle method onDestroy
, that Svelte provides to verify that the layout component renders only once and is never unmounted on navigating to different pages. By adding these lines to $layout.svelte
, you’ll notice that no log appears in the console:
import { onDestroy } from 'svelte'; onDestroy(() => console.log("$layout unmounted"));
onDestroy
never gets called even when we navigate between pages.
We can use this behavior to our advantage by fetching some data that many pages need or setting up a centralized store (which we will see later) that other pages can use to pass data to each other.
If you’re familiar with Svelte or React, adding context to the code saves us from prop drilling. In our example, we can add context for data in $layout.svelte
for all the pages and their components to receive.
We know that SvelteKit, by default, renders the app on the server side during the first load. But what if we wanted to populate our app with data during SSR without showing the users a loading spinner? Or, how do we pass data from the server to the client side?
Well, SvelteKit provides hooks that run only on the server and help us achieve these goals. But before we explore hooks, I want to talk about endpoints to better understand the server side.
Endpoints are server-side and are created similarly to pages and routes. However, files that are endpoints will end with a .js
or .ts
extension in the routes
directory.
// src/routes/dogs.ts import type { RequestHandler, Response } from "@sveltejs/kit"; interface dog{ name: string } const dogs:dog[]=[{name:"German Shepherd"},{name:"BullDog"},{name:"Poodle"}] export const get:RequestHandler= async () =>{ const res:Response={ body:{ dogs } } return res; }
The method name get
corresponds to the HTTP method GET. This endpoint is available at /dogs
. If you navigate to /dogs
in your browser, you will find a JSON response containing the list of dogs.
With hooks, you have finer control over the server side, creating an ideal place to perform functions like authentication because they also receive the HTTP request object from the client. There are three hooks in SvelteKit, and we will be using getContext
and getSession
in the next section.
Understanding the basics of the SvelteKit ecosystem, we can build a very simple toy application that will fetch data from a source that we’ll set up, perform some simple authentication, and set up a central store.
Our app will contain the following routes: /counter1
, /counter2
, /about
, and /login
. The Counter pages will be protected and the About page will not.
So let’s focus on the authentication logic first.
Since the hooks run on the server on each request before anything else runs, and because they have access to the request parameters, src/hooks.ts
is the ideal place to extract cookies and create a session for the user.
Note that the session is not a session in its typical sense; the server side will not keep any record of the sessions. The session we will use here will simply help us pass data to the client side and provide the initial state.
The getContext
hook receives the request headers, which may or may not contain cookies, depending on the authentication of a request. When we extract the authentication token and return it, the next hook will receive this context as a parameter.
Anything returned from the getSession
hook is available to every page as a session variable.
// src/hooks.ts import {defaultState} from '$lib/store'; import * as cookie from 'cookie'; const auth_token='demo_token_for_example'; const userDetails={name:"Deb",age:45} export const getContext:GetContext=({ headers })=>{ const cookies = cookie.parse(headers.cookie || ''); return { token:cookies['token'] }; } export const getSession:GetSession=async ({context})=>{ let initialState={...defaultState}; if (context['token']===auth_token){ console.log("tokens match"); initialState.authenticated=true initialState.user=userDetails; } console.log(initialState) return initialState }
For the sake of brevity and simplicity, we’ll store the authentication token and user details in the file itself. In a real project, you would probably use a database for this or an authentication backend.
The idea is to extract a cookie from the headers in getContext
then check if it has the right token. If it contains the right token, we return the “authenticated” initial state. Don’t worry about the initialState
, we’ll take a look at $lib/store
later in this post.
We’ll now set up an endpoint that will accept a GET request and return a cookie containing the token. This will be useful in the login component.
// src/routes/auth.ts const auth_token='demo_token_for_example'; const cookie=`token=${auth_token};HttpOnly;Secure` const header:Headers={'set-cookie':cookie} export const get:RequestHandler=()=>{ return{ headers:header, body:{ token:auth_token, success:true, user:{ name:"Deb", age:45 } } } }
Again, the user details will be typically fetched from a database. But here, we’re hardcoding them for simplicity.
If you’re not familiar with Svelte’s writable stores, they can be written to and from anywhere within the app and are reactive. This is a simple way to set up a writable store that will store the global state of our application.
// src/lib/store.ts import {Writable, writable} from 'svelte/store'; export type User={ name:string|null, age?:number } export interface stateType{ authenticated:boolean, user:User, counter:number } export const defaultState:stateType={ authenticated:false, user:{ name:null, }, counter:0 } export default class Store{ state:Writable<stateType>; constructor(initialState:stateType=defaultState){ this.state=writable({...initialState}) } changeAuthenticationState=(user:User)=>{ this.state.update((obj)=>{ console.log("old state") console.log(obj) return { ...obj, authenticated:!obj.authenticated, user:user } }) } updateCounter=(val:number)=>{ this.state.update((obj)=>{ return { ...obj, counter:val } }) } }
Next, we’ll set up a context at the $layout.svelte
root and provide our store to all the descendants, enabling all the pages to access store.
<!-- src/routes/$layout.svelte --> <script context="module" lang="ts"> import Store from '$lib/store'; import {setContext} from 'svelte'; </script> <script lang="ts"> import '../app.css'; import {session} from '$app/stores'; const store=new Store($session) setContext<Store>('store',store); </script> <slot />
Notice how we’re creating a new store using the initial state we received from the session and passing it to setContext
. The store can now be accessed in any page by the key 'store'
.
load
functionOur pages can also export a special function called the load
function. This function can fetch data or write to the session before the component renders, first running on the server side and then on the client side. This is especially useful during server-side rendering, as we might need to populate our page with data that must be fetched beforehand.
<!-- src/routes/login.svelte --> <script context="module" lang="ts"> import type { Load } from '@sveltejs/kit'; export const load:Load=async ({session})=>{ if(session.authenticated){ return{ redirect:'/counter1', status:302 } } return {} } </script> <script lang="ts"> import type Store from '$lib/store'; import {goto} from '$app/navigation'; import {setContext,getContext} from 'svelte'; const store=getContext<Store>('store'); const login=async ()=> { let res= await fetch('/auth'); let data=await res.json(); if(data.success){ store.changeAuthenticationState(data.user); goto('/counter1'); } } </script> <h1>Login Page</h1> <button on:click={login}>Login</button>
In the load
function of the Login page, we can check whether the user is authenticated since we don’t want to display the Login page to the authenticated user.
If they are authenticated, we redirect them to the /counter1
page. If not, we fetch the token and update the state. Once authenticated, we can navigate to the protected routes like the /counter1
.
The load
function of counter1.svelte
checks if the user is authenticated and redirects them to the Login page if they are not. We perform this check only on the server side since our app is structured in a way that it does not provide any way to navigate to the /counter1
page without performing a full request to the server.
<script context="module" lang="ts"> import {browser} from '$app/env'; export const load:Load=async ({session})=>{ if(!browser) { if(!session.authenticated){ return{ redirect:'login', status:302 } } else{ session.counter=1; //set counter to 1 during ssr } } return {} } </script> <script lang="ts"> import type Store from '$lib/store'; import Counter from '$lib/Counter.svelte'; import {setContext,getContext} from 'svelte'; const store=getContext<Store>('store'); const state=store.state; </script> <svelte:head> <title>Counter 1</title> </svelte:head> <main> <h1>Hello {$state.user.name}</h1> <Counter update={store.updateCounter} count={$state.counter}/> <p>Visit <a href="/counter2"> Counter2</a> </p> </main>
However, we don’t include links to the protected pages in any unprotected page, so there’s no way to navigate to these without a full load. This means a request to the server will be made.
When a request for /counter1
is made, getSession
runs and assigns the initial state, which sets the counter to 0. The load
function then runs and updates the counter value to 1, sending the updated session to the layout component to set up the store with the updated state.
Note that if we had a load function in $layout.svelte
, it would run before the load function of counter1.svelte
.
The /counter2
page is the same as /counter1
except we initialized the counter to 2, prompting Line 13 to become session.counter=2
.
In the following code, we can use the counter component in both the /counter1
and /counter2
pages:
<!-- Counter.svelte --> <script lang="ts"> export let count:number; export let update:Function; const increment = () => { update(count+1) }; </script> <button on:click={increment}> Clicks: {count} </button>
To finish up the app, we must add the about.svelte
page:
<!-About.svelte --> <h1> About page </h1>
npm run build
will create a production build for us. Since we’re using the default node adapter, we get a node server in /build
and serve the app using node build
.
By using SvelteKit, we were able to create an app containing SSR, authentication, and a store in just a few minutes!
Since SvelteKit’s framework is still in beta, it can be difficult to find answers if you face any problems while working with it. However, if it suits your project requirements, it can be incredibly effective.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowconsole.time is not a function
errorExplore the two variants of the `console.time is not a function` error, their possible causes, and how to debug.
jQuery 4 proves that jQuery’s time is over for web developers. Here are some ways to avoid jQuery and decrease your web bundle size.
See how to implement a single and multilevel dropdown menu in your React project to make your nav bars more dynamic and user-friendly.
NAPI-RS is a great module-building tool for image resizing, cryptography, and more. Learn how to use it with Rust and Node.js.
2 Replies to "Exploring SvelteKit, the newest Svelte-based framework"
Sooo…. How do you integrate LogRocket with Sveltekit, Debjyoti Banerjee?
Using LogRocket with SvelteKit shouldn’t present any issues. If you’re using SSR, you might want to take a look at this page in our docs.
If you have specific questions, feel free to get in touch with us via the chat app on our homepage.