Different Ways of Loading Data in Sveltekit

Different Ways of Loading Data in Sveltekit

·

11 min read

In this article, we are going to take a look at how to load data from the latest version of Sveltekit after some breaking changes such as the + prefix that now defines the file as being a route according to this release update: https://github.com/sveltejs/kit/discussions/5748

A load function in a +page.js file runs both on the server and in the browser.

However, for security purposes, if your load function must always run on the server because it needs to use private environment variables or to make database calls then you need to use +page.server.js instead.

You can also run the layout.server.js for the server side only.

A +page.svelte or +layout.svelte fetches its data from a load function. If the load function is defined in +page.js or +layout.js then it will run both on the server and the client side.

In both cases, it must return an object like this:

src/routes/+page.js
/** @type {import('./$types').PageLoad} */
export function load(event) {
  return {
    data: `some data goes here`
  };
}

To try to understand these concepts we will create a simple application using Sveltekit:

npm create svelte@latest page-load
cd page-load
npm install
npm run dev

Make sure you select plain Vanilla Javascript and Skeleton template instead of the Sveltekit demo for this tutorial.

Next, you will need to create 2 folders under src/routes, one called quotes and another called users and also create 2 files: +page.js and +page.svelte inside each folder.

We should also create these 2 template files: +layout.svelte and Header.svelte files.

routes/+layout.svelte
<script>
import Header from './Header.svelte';
</script>

<main style="margin:auto">
    <Header></Header>
    <slot />
</main>

<style>
    main {
        margin: auto;
        width: 800px;
    }
</style>
routes/Header.svelte
<nav>
    <a href="/">Home</a>
    <a href="/quotes">Quotes</a>
    <a href="/users">Users</a>
</nav>

<style>
    nav {
        display: flex;
    }
    nav a {
        margin-right: 1rem;
    }
</style>

Data fetching via Load function

Now, we will edit our quote/+page.js to make a fetch call to a dummy API to get some popular quotes.

To make a data fetch request we need to set up our load function which should reside in a separate file called +page.js.

Before we can make any external fetch requests we need to bring in Sveltkit's { fetch } built-in utility which should be passed in as an input method to our load function.

Sveltekit built-in utility fetch is equivalent to the native fetch web API, with a few additional features that we will not be able to cover in this article.

Continuing, to load some data on our quotes page, we define our load function below:

routes/quotes/+page.js
export const load = async ({ fetch }) => {
  const result = await fetch('https://dummyjson.com/quotes?limit=10');
    const data = await result.json();
    const quotes = data.quotes
    return {
        quotes: quotes
    }
}

If you don't pass fetch as an argument to the load() function, JavaScript will try to find a fetch function in the global scope. If a global fetch function is present, it will be used. This is why it may work in some cases, even if you don't pass fetch as an argument. However, it's important to note that in some environments such as server-side rendering, the global fetch function is not available and it will not work.

It is always better to explicitly input what you are trying to use. Passing fetch as an argument to the load() function makes it clear that you are using SvelteKit's fetch utility, which has additional features and functionalities that the standard fetch API does not have.

Overall, you are not required to pass fetch as an argument to the load() function, but it's a best practice to do so because it makes the code more explicit and it ensures that you're using SvelteKit's fetch utility, which has additional features and functionalities that the standard fetch API does not have.

Here is the code to access the data object from our +page.js inside our +page.svelte:

routes/quotes/+page.svelte
<script>
    export let data;
    console.log(data);
</script>

If you refresh the page and click on Quote, you can see inside our quotes/+page.svelte that we have obtained our quotes in the console:

If you expand quotes we can see that we have an array with all 10 quotes:

Also, notice that if you move away and click on Home then on Quotes, the load function will run the fetch call again to our API both on the server and on the client site.

Next, we will use the data that we just fetched from our Load function and will use it in our +page.svelte by destructuring the data object in the quotes variable like this:

routes/quotes/+page.svelte
<script>
   export let data;
   const { quotes } = data;
</script>

IMPORTANT: thedata prop is not explicitly defined in the +page.js load function, but its return object is available to the +page.svelte via the data prop:

After understanding that we can now render out the data in our html pretty simply by using svelte blocks through our dummyjson.com/quotes?limit=10:

<script>
    export let data;
    const { quotes } = data;
    console.log(data);
</script>

<h1> Quotes </h1>

{#each quotes as item} 
<h2>{item.author}</h2>
<p>{item.quote}</p>
{/each}

Pay attention that If we navigate back and forward between navigation links we can see that our Quotes content is being rendered on the first request on a server site and the client side is rendered on subsequent requests:

Multiple Fetches Requests in Parallel

Now, let's make things a little bit more complicated assuming that we want to make multiple API calls.

For this purpose, we will now make use our users Dummy JSON API data:
https://dummyjson.com/users?limit=10

But, to keep it simple, let's modify our Quotes +page.js to make two fetch requests like this:

routes/quotes/+page.js
export const load = async ({ fetch }) => {
    // First Fetch Request to Quotes
    const resultQuotes = await fetch('https://dummyjson.com/quotes?limit=10');
    const quotesData = await resultQuotes.json();
    const quotes = quotesData.quotes

    // Second Fetch Request to Comments
    const resultUsers = await fetch('https://dummyjson.com/users?limit=10');
    const data = await resultUsers.json();
    const users = data.users

    return {
        quotes: quotes,
        users: users
    }
}

Observe that, when we navigate to Home page and then to Quotes by clicking on the links we can see a chained waterfall behavior between these two fetch requests.

The drawback of this behavior is that the user's requests need to wait for the Quotes request to finish before it even starts. Depending on the network bandwidth, this can frustrating and cause the user to move away from the page.

Now, for learning purposes, let's assume these two requests are independent of each other.

If you need to request data from multiple sources independently, then there is a small change in our output that we can do to improve the user experience according to official Sveltekit documentation:

"A universal load function can return an object containing any values, including things like custom classes and component constructors.

A server load function must return data that can be serialized with devalue — anything that can be represented as JSON plus things like BigInt, Date, Map, Set and RegExp, or repeated/cyclical references — so that it can be transported over the network."

In the documentation, it says that "top-level promises will be awaited, which makes it easy to return multiple promises without creating a waterfall":

Considering that, let's make a slight modification in our +page.js for our Quotes to easily return multiple promises that will be resolved and awaited on the page without creating the undesired waterfall effect.

For this task, we will need to create two async functions like this:

routes/quotes/+page.js
export const load = async ({fetch}) => {
    // First Fetch Request to Quotes
    const fetchQuotes = async () => {
        const resultQuotes = await fetch('https://dummyjson.com/quotes?limit=10');
        const quotesData = await resultQuotes.json();
        const quotes = quotesData.quotes
        return quotes;    
    }

    // Second Fetch Request to Comments
    const fetchUsers = async () => {    
        const resultUsers = await fetch('https://dummyjson.com/users?limit=10');
        const data = await resultUsers.json();
        const users = data.users
        return users;    
    }

    return {
        quotes: fetchQuotes(),
        users: fetchUsers()
    }
}

According to the documentation, Top-Level promises will be awaited, which makes it easy to return multiple promises without creating a waterfall.

Finally, we can see that these two fetch requests are running in parallel. This is a recommended strategy when the requests are not related and can be run independently.

Pay attention to these 3 modifications we made:

  1. The conversion of each API request to be an async function;

  2. We added a return for each promise;

  3. On the return of the load function we modified our return object to return these two promises functions.

Pre-Fetching Data

Another cool feature of SvelteKit is pre-fetching. Let's say we have users on slower connections such as 3G or edge and we want to improve the overall experience of our site.

What we can do about it without much effort? just add data-sveltekit-prefetch to the link you want the desired behavior.

In our navigation Menu at the top, we can have SvelteKit pre-fetch the data when the user hovers over the link.

routes/+Header.svelte
<nav>
    <a href="/">Home</a>
    <a href="/quotes" data-sveltekit-prefetch>Quotes</a>
    <a href="/users">Users</a>
</nav>

<style>
    nav {
        display: flex;
    }
    nav a {
        margin-right: 1rem;
    }
</style>

For instance, when the user simply hovers over the Quotes link, a fetch request is made that automatically loads the page behind the scenes and, as soon as we click it, it instantaneously downloads it significantly improving user's experience.

That small amount fraction of time will still make a difference in the overall user experience.

Fetching Data on the Server Side using +page.server.js

In SvelteKit, you can fetch data using a +page.server.js file, which is a server-side script that runs before the page is rendered on the client. This file can be used to fetch data from an API or a database and make it available to the Svelte component on the client.

This type of approach is recommended when data security is an issue and you have data that do not want to expose on the client side such as API keys or database connections.

A +page.server.js is recommended to use when you want to fetch data from an API or a database that is only available on the server and that should be preloaded before the page is rendered on the client. This allows you to make the data available to the Svelte component as soon as it is rendered, without the need for an additional network request.

For this strategy, we will use the Mockaroo API to generate fake user data that can only be accessed via an API key. And to use this free API generator you will need to sign in.

As soon as you log in to Mockaroo create a schema called users_schema then click Save this Schema at the bottom of thepage. Next, you will be prompted with a bar at the bottom of the page screen to create an API.

The last step is to create a Mock API that we will use in our Sveltekit app by clicking on Create Mock API at the bottom:

This page lists all Mock API that you have created:

You will then be able to access your endpoint via URL passing the key via URL as a query param or via Header param using POSTMAN.

https://my.api.mockaroo.com/users_schema.json?key={YOUR_API_KEY}

Enough!!! with FAKE API endpoint implemented, let's try this server fetch call API key using our SvelteKit simple application.

In our Sveltekit project let's first create an environment file ".env" at the root of src folder and add this line api_key={YOUR_API_KEY}

run npm i dotenv // this is necessary to access the content of .env later.

Let's implement the code for server-side rendering that requires an API key:

routes/users/+page.server.js
import 'dotenv/config';
export const load = async () => {
    const fetchUsers = async () => {
        const result = await fetch(`https://my.api.mockaroo.com/users_schema.json?key=${process.env.users_api_key}`);
        const data = await result.json();
        return data;
    }
    return {
        users: fetchUsers()
    }
}

Note that, unlike the +page.js, the +page.server.js does not take in the fetch method from Sveltekit. We just use the regular fetch request. For this, you would a regular standard Node.js API like fetch() or a library like Axios.

On chrome dev tools console, we can see our users array:

Now, we can render this data in our +page.svelte like this:

routes/users/+page.svelte
<script>
    export let data;
    const { users } = data;
    console.log(data);
</script>

<h1> Users </h1>

{#each users as user} 
<h2>{user.first_name} {user.last_name}</h2>
{/each}

To confirm that this +page.server.js file only runs on the server side we can comment out the console.log(data) in our +page.svelte and add it just before we return the data like this:

routes/users/+page.server.js
import 'dotenv/config';
export const load = async () => {
    const fetchUsers = async () => {
        const result = await fetch(`https://my.api.mockaroo.com/users_schema.json?key=${process.env.users_api_key}`);
        const data = await result.json();
        console.log(data); 
        return data;
    }
    return {
        users: fetchUsers()
    }
}

The main difference between these two types of pages (+page.js and +page.server.js) is that the fetch method never gets passed into the server-side page.

Now, you should not see any results in the console tab or the network tab because this code is running entirely on the server side.

Another cool thing about Sveltekit is that we can apply the same pre-fetch prefix in our navigation link for the User's page.

Don't forget to remove +page.js or just rename it to page.js in order to experiment with +page.server.svelte only.

routes/+Header.svelte
<nav>
    <a href="/">Home</a>
    <a href="/quotes" data-sveltekit-prefetch>Quotes</a>
    <a href="/users" data-sveltekit-prefetch>Users</a>
</nav>

<style>
    nav {
        display: flex;
    }
    nav a {
        margin-right: 1rem;
    }
</style>