5

Deep Dive into Data Loading

Every application ever created has been driven by data. Without data to process, an application is effectively useless. That’s why it’s very important for developers to have a firm understanding of how to manage the retrieval of that data for their application. When working with SvelteKit, this is done by exporting a load() function in page or layout files.

In the previous chapter, we briefly touched on load(). In this chapter, we’ll analyze it further by discussing how it works and by looking at more practical, real-world examples of making use of it. We’ll create an example of forcing load() in the client only as well as covering some key details to remember when using load(). We’ll also use load() in layouts to showcase how it can make data portable across our application. Finally, we’ll look at an example of making use of some of the data provided in a server load() function that is unavailable in universal load() functions.

We’re going to cover the following topics in this chapter:

  • Loading in Clients
  • Loading in Layouts
  • Destructuring RequestEvent

By the time you’ve finished this chapter, you’ll be comfortable with all the various ways you can load data in your SvelteKit application.

Technical requirements

The complete code for this chapter is available on GitHub at: https://github.com/PacktPublishing/SvelteKit-Up-and-Running/tree/main/chapters/chapter05.

Loading in Clients

While discussing Creating Server Pages in the previous chapter, we covered how a load() function exported from +page.js will run on both the client and the server. When we want to ensure load is only run on the server, we move it to +page.server.js. But what if you’re trying to build an offline-ready application? You may be building a Progressive Web App (PWA), a Single-Page App (SPA), or both! For the sake of demonstration, let’s assume you want as much logic as possible to be managed on the client rather than your server. In this case, you’ll want load() functions to run on the client and not on the server. How can we do that when a load() function from +page.js runs in both environments?

Again, think back to the Creating Server Pages section in the previous chapter where we discussed page options, and you’ll remember the ssr option. When exported, this constant will disable or enable Server-Side Rendering based on the Boolean value we’ve provided it. To make a load() function in +page.js run only in the client, we can add export const ssr = false;. Let’s go back to our fetch example from Chapter 3 and modify it to demonstrate this.

Before making this adjustment, ensure the console.log('got response') function still exists. Open the /fetch route in your browser and confirm the output is shown in both the browser console and your development server. Once you’ve done so, disable SSR on the page by exporting the ssr page option:

src/routes/fetch/+page.js

const key = 'DEMO_KEY'; // your API key here
export const ssr = false;
export function load() {
  const pic = fetch(`https://api.nasa.gov/planetary/apod?api_key=${key}`)
    .then(response => {
      console.log('got response');
      return response.json();
    });
  return {pic};
}

This example is identical to when we saw it earlier, except that on line 3, we’ve added export const ssr = false;. This page option effectively disables SSR for the page, meaning that load() is only ever run in the client. You’ll notice the console.log() call isn’t output to the development server anymore but does show in the browser console.

From here on out, we’ll differentiate load() functions as either universal or server. Obviously, server load() functions are run on the server in +page.server.js, which means that universal load() functions are run from +page.js. At a high level, they are functionally identical. But there are a few idiosyncrasies to mention:

  • Both universal and server load() functions can access data related to the request that called it.
  • A server load() function will have access to more request data, such as cookies and the client IP address.
  • Universal load() functions always return an object. The values of that object may be nearly anything.
  • Server load() functions must return data that can be serialized by the devalue package (essentially, anything that can be converted into JSON). Find out more about devalue at https://github.com/rich-harris/devalue.

Universal Load Timing

It should be mentioned that on the first render, load() will execute on the server and client. Each subsequent request will then be executed in the client only. To demonstrate this behavior, navigate your browser to a route that has load() run from a +page.js file, like our /fetch example from Chapter 3. Observe the console output in the server as well as the client when initially opening the /fetch page in your browser. Navigating to another route and back will show the output only in the client.

One final note about calling load(); it will always be invoked at runtime unless you have specified that the page should be prerendered by way of page options. If you have decided to prerender the page, then load() will be called at build time. Remember that pages should only be prerendered if the static HTML shown should be the same for each user to access the page.

We’ve just covered how load() can be forced to run only on the client and some details about how it works. With all of this new information as well as the information from previous chapters, you should feel relatively comfortable about the fundamentals of load(). Let’s expand on it and take a look at how it might be used in a layout template.

Loading in Layouts

So far, we’ve only looked at load() being used in +page.js or +page.server.js files, but it can also be utilized in +layout.js or +layout.server.js files. While layouts cannot export actions, they are otherwise functionally identical to page files. This means that previously mentioned page options (such as ssr) and load() functions will apply to any components nested inside of the layout. Another important quality to understand about load() functions is that because they are run concurrently within SvelteKit, a single page will not render until all requests have completed. Having a load() function on a page as well as a layout will prevent rendering until both have completed. But because they will be run simultaneously, any delays should be negligible.

When loading data in a layout, the most obvious advantage of doing so is the ability to access that data in sibling and child pages. This means that any data loaded by a layout can then be accessed within an inherited +page.svelte file when it has exported the data variable. SvelteKit will also keep track of data loaded across the application and only trigger load() when it believes it to be absolutely necessary. In instances where we want to force data to be reloaded, we can import the invalidate or invalidateAll modules provided by $app/navigation.

To demonstrate these concepts, let’s create a component alongside the navigation that can alert the user to unread notifications. The component will persist across the application header so it may be easily accessed. This makes for an ideal scenario showcasing loading data from a layout. We’ll also create another page that shows the full list of notifications to demonstrate how data loaded from a layout can be used in a child component.

Let’s start with the load() function in +layout.js. For simplicity’s sake, we’ll return the data directly within the function call instead of making a call to an imaginary database or API:

src/routes/+layout.js

export function load() {
  console.log('notifications loaded');
  return {
    notifications: {
      count: 3,
      items: [
        {
          type: `comment`,
          content: `Hi! I'm Dylan!`
        },
        {
          type: `comment`,
          content: `Hi Dylan. Nice to meet you!`
        },
        {
          type: `comment`,
          content: `Welcome to the chapter about load()!`
        }
      ]
    },
  }
}

This file consists only of the exported load() function, which returns an object containing another notifications object. Remember that universal load() functions can export anything so long as it resides inside an object. The notifications object is quite simple as it consists of two properties; a count property with the value of 3 and another property labeled items, which is just an array of three other objects. To show how the data isn’t loaded every time we navigate to a new page, we’ve included a console.log() call that outputs the text notifications loaded.

Next, we’ll make some changes to our root layout template so it can actually use our freshly loaded data. For the most part, it will stay the same, but we’ll need to add some markup that can show the data as well as minimal styling to convey the concept of a notification badge:

src/routes/+layout.svelte

<script>
  import Nav from '$lib/Nav.svelte';
  import Notify from '$lib/Notify.svelte';
  export let data;
</script>
<div class='wrapper'>
  <div class='nav'>
    <div class='menu'>
      <Nav />
    </div>
    <div class='notifications'>
      <Notify count={data.notifications.count}/>
    </div>
  </div>
  <div class='content'>
    <slot></slot>
  </div>
  <div class='footer'>
    This is my footer
  </div>
</div>
<style>
  .wrapper {
    min-height: 100vh;
    display: grid;
    grid-template-rows: auto 1fr auto;
  }
  .footer {
    text-align: center;
    margin: 20px 0;
  }
  .nav {
    text-align: center;
  }
  .menu {
    display: inline-block;
  }
  .notifications {
    float: right;
  }
</style>

Here are some important changes to make note of in this version of +layout.svelte:

  • A new Notify component is imported (shown next).
  • We exported the data variable to make use of the data returned from src/routes/+layout.js.
  • The notifications count property is sent to the Notify component.
  • The markup for the .menu and .notifications elements are added to the .nav div element. This allows us to show the Notify component in the top-right corner of the page.
  • New styles for elements with the .nav, .menu, and .notifications classes are added to style our new markup.

Next, let’s look at the Notify component we just imported. This component will contain the markup that shows our notification count and links to the /notification route:

src/lib/Notify.svelte

<script>
  export let count = 0;
</script>
<a href='/notifications'>
  {count}
</a>
<style>
  a {
    padding: 15px;
    color: white;
    text-decoration: none;
    background-color: #ea6262;
  }
</style>

This component is relatively simple. Firstly, it exports the count variable and gives it a default value of 0. This is necessary because, while this component is used inside the layout, it does not exist underneath or alongside the +layout.js file we created earlier and so it does not have access to the information provided by the layout load() function. Next, this component creates a link tag to contain the count variable. And finally, it contains spartan styling to decorate our notification badge.

Finally, let’s look at the notifications page. Because this file exists underneath the hierarchy of +layout.js, we can access data as if it were loaded from a +page.js file that existed alongside it:

src/routes/notifications/+page.svelte

<script>
  export let data;
</script>
{#if data.notifications.count > 0}
  <ul>
  {#each data.notifications.items as item}
    <li>{item.content}</li>
  {/each}
  </ul>
{/if}

This page makes use of Svelte directives: {#if} and {#each}. Since we exported the data variable at the top of the component, we can use data loaded from src/routes/+layout.js within this component. If the count property of the notifications objects is greater than zero, it will create the markup necessary for an unordered list. It then outputs the content property of each comment item inside a list item.

Now, when you open your project in your browser, you should see a new notification badge displayed in the top-right corner of the app showing the value of the count property from the notification object. Try selecting some of the items in the navigation menu and see how the text notifications loaded isn’t output every time you click a link. It is shown on the initial load in both the development server as well as the browser console but not run again. That is because the data being loaded has yet to change, and SvelteKit recognizes this.

Let’s look at forcing the data to be reloaded when we click on the notification badge. We can do this by using invalidateAll imported from $app/navigation. If the load() function used fetch(), it would make sense to use the invalidate module instead. In that instance, we would force the reload by passing the URL specified inside of the fetch() call to invalidate(). Since we’re simply returning an object, we’ll need to use invalidateAll() to trigger the reload:

src/lib/Notify.svelte

<script>
  import { invalidateAll } from '$app/navigation';
  export let count = 0;
</script>
<a href='/notifications' on:click={() => invalidateAll()}>
  {count}
</a>

In the Notify.svelte component, we’ve added the import of invalidateAll. When the notification link badge is clicked, it calls invalidateAll(), informing SvelteKit to rerun all load() functions within the context. Now, when you click the notification link at the top of the page, you should see the browser console output notifications loaded. Navigating to other pages such as About, News, or Home will not produce the output.

In the future, should you find yourself building components that will be showing dynamic data across an application’s interface, consider the concepts we’ve just covered. By loading data in layout files, you can reduce the number of HTTP requests or database queries made, which can significantly improve the experience of the application for your users. And should you need to force that data to be reloaded, you’ll know how to go about invalidating data so SvelteKit will re-run the appropriate load() functions. Next, let’s take a look at how load() can be leveraged further to build more advanced functionality.

Destructuring RequestEvent

When it comes to load(), the server seems to have more information available to it than the client does. With so much data, it can be hard to know exactly all the information that is available. In short, server load() functions are called with a SvelteKit-specific RequestEvent. Here’s a quick breakdown of the properties (prop) and functions (fn) available from that object:

  • cookies (prop) – The cookies sent during the request.
  • fetch (fn) – A compatible variant of the Web API fetch() function discussed in Chapter 3. It comes with the added benefit of allowing requests based on relative routes as well as passing cookies and headers through when on the same server.
  • getClientAddress (fn) – Returns the client’s IP address.
  • locals (prop) – Any custom data inserted into the request via SvelteKit’s handle() hook. We’ll cover that in a later chapter.
  • params (prop) – Parameters specific to the current route, such as the article slug passed to the news example in the previous chapter.
  • platform (prop) – Data added by the environment adapter.
  • request (prop) – The actual request data represented as an object.
  • route (prop) – The identifier of the requested route.
  • setHeaders (fn) – Allows for the manipulation of headers in the returned Response object.
  • url (prop) – Data about the requested URL, which we covered in Chapter 3.

RequestEvent demo

To see this information for yourself, create the src/routes/+layout.server.js file with a console.log() function outputting a single passed-in argument to load(). By creating it in the root layout, you’ll be able to see how properties change based on the different routes accessed from your browser. The data will then be shown in your development console.

A practical example where you may find yourself needing to utilize this data is in the case of user authentication. Normally, after a user has authenticated, they are given a cookie (for doing such a good job entering their password – pun intended) to store on their device, which ensures their authentication will persist for the duration of their visit. If they leave the application, it can later be used to confirm their identity so they aren’t required to authenticate yet again. Let’s observe how this might be accomplished with SvelteKit. Since this chapter is about load(), we’ll build the actual form and discuss how to set the cookies in the next chapter. For now, we’ll simply check whether the user has a cookie set and set one manually in the browser.

To begin, let’s rename src/routes/+layout.js to src/routes/+layout.server.js. If we’re going to access cookie data, we’ll need access to the data provided by RequestEvent. By adding the logic to our root server layout, we have the added benefit of keeping the authentication checks in place across the entire application:

src/routes/+layout.server.js

export function load({ cookies }) {
  const data = {
    notifications: {
      count: 3,
      items: [
        {
          type: `comment`,
          content: `Hi! I'm Dylan!`
        },
         …
      ]
    }
  };
  if(cookies.get('identity') === '1') {
    // lookup user ID in database
    data.user = {
      id: 1,
      name: 'Dylan'
    }
  }
  return data;
}

In this new version of the root layout logic, we’ve destructured the argument passed to load() since we currently only need access to the cookies property. We kept the notifications object we created earlier but put it inside a new variable called data. This text also omits a couple of entries for the sake of brevity. From there, we check whether the request sent to our application contained a cookie by the name of user with the value of 1. If it did, we insert some fake user information into the user property of the data object. Normally at this point, we would check the cookie value against valid sessions in a database, and if one was found, we would then retrieve the appropriate user data and send that back to the client, but we’re trying to keep it simple. After all of that, the data object is returned from load().

Next, we’ll need to actually show that the user has been successfully authenticated. To do this, we’ll create a new route where our user can log in:

src/routes/login/+page.svelte

<script>
  export let data;
</script>
{#if data.user}
  <p>
    Welcome, {data.user.name}!
  </p>
{/if}

As we covered in the last section, the data returned from +layout.js or +layout.server.js is made available in child components by exporting the data variable. Once that is done, we use the Svelte {#if} directive to check whether we have the user property set. If found, we then display the name property of data.user.

Of course, nowhere in this example do we ever set a cookie. We’ll cover that in the next chapter so, for now, let’s manually create the cookie in our browser. Before doing so, navigate to the /login route and verify that nothing is shown on the page. Once you have confirmed it is a blank page, go ahead and create the cookie using the following steps for your browser:

  • Firefox:
    1. Open Developer Tools by using the F12 key. Alternatively, open Menu | More Tools | Developer Tools.
    2. In Developer Tools, select the tab labeled Storage.
    3. Under Cookies, select the project URL. Doing so should show all available cookies for the domain.
    4. Click the + symbol to add an item.
    5. Double-click the name field and enter the text identity.
    6. Double-click the value field and enter the text 1.
    7. Ensure the path field is set to / using the same steps.
  • Chrome:
    1. Open Developer Tools by using the F12 key. Alternatively, open the Menu | More Tools | Developer Tools.
    2. In Developer Tools, select the tab labeled Application.
    3. In the Storage section, select the Cookies menu item.
    4. Select the project URL.
    5. In the empty windowpane, select the name column and enter identity.
    6. Select the value column and enter 1.
    7. Ensure the path field is set to / using the same steps.

Having followed these steps, you should now have the correct cookie in your browser. After doing so, refresh the /login page in your browser and you’ll see a message welcoming the user with the value from the name property specified. This example is quite simple and actual cookie-based login systems are functionally slightly more complicated; however, the concepts remain the same.

While the example we covered only made use of the cookies property from RequestEvent, we saw how trivial it would be to access any of the other properties such as url and params, or even to set our own headers with the setHeaders function. With all of that data available to us, the possibilities of what could be built into our application are nearly limitless.

Summary

In this chapter, we covered a lot of information about load(). We first discussed how it can be done only in the client and then moved on to some finer details about how it works. After that, we looked at using load() in layouts to minimize the number of requests made for each page load and maximize convenient access to data that may be needed application-wide. We also looked at invalidating data in cases where we would want data to be reloaded. Finally, we covered how server load() functions are called by RequestEvent, which gives us access to so much more valuable information. That information can enable us to build cookie-based login functionality for our application.

Having spent this chapter learning about some of the finer details behind load(), you should feel comfortable taking a load off and relaxing. If you have any baked cookies to hand, I suggest you take a break from the book and treat yourself to some. You’ve earned it.

But do come back because, in the next chapter, we’ll cover more of the finer details behind receiving data from users through the use of forms, making the forms fun, and reducing the friction of data entry by utilizing snapshots.

Resources

devalue: https://github.com/rich-harris/devalue

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset