Captain Codeman Captain Codeman

De-duplicate Page Data Updates with SvelteKit

Prevent store re-calcs and re-renders

Contents

Introduction

The latest version of SvelteKit (currently 1.0.0-next.445) aggregates data loaded from the current page and any parent layouts’ data load functions into a convenient-to-use Svelte store. This page store also includes route id, parameters and the current URL.

It’s the same data that will also be passed to the data property in each layout or page route (typed as PageData and LayoutData respectively).

It’s not unusual to want to have some global root site settings or configuration (especially in a multi-tenanted scenario) and also some per-user data such as session state.

Often, this data will be used throughout our app in several places when passing things as props would be inappropriate and it will often need to be used as input into other state in non-svelte components. As an example, suppose the “site config” data contains currency and price information for the entire site or the current category and also some discount parameters for multiple purchases. This would make sense to combine with cart information using a Svelte derived store to calculate cart totals and offer prices as someone used the site.

There are two approaches, both with their problems.

Set store from data prop

The first is simply to set our own custom store whenever the layout data changes. Imagine we create a store that we can import whenever and wherever we need to use this data in our app:

/src/lib/stores/site.ts

import { writable } from 'svelte/store'

export const site = writable()

In our /+layout.svelte file we can set that store data whenever the data prop changes:

<script lang="ts">
  import { site } from '$lib/stores/site'
  import type { LayoutData } from './$types'

  export let data: LayoutData

  $: $site = data.site // or site.set(data.site)
 <script>

It will update the store when the page first loads and any time we trigger a data load using invalidate().

The draw-back is it’s a little boilerplatey as we’ve added code to a page that may not itself need or use it. It’s only a few lines but imagine the number of features in your app grows and with it the number of stores you want to keep information in? Also, we’ve immediately introduced a potential issue - the store is writable when it shouldn’t really be. It has to be, because we need to set it’s value externally.

This may not be an issue to you in which case you can shake your head at this whole thing and move on.

Use a derived store

We can address the “writable” issue by using a derived store. Remember we also have this data in the page store? We can use a derived store to pluck it out:

import { derived } from 'svelte/store'
import { page } from '$app/stores'

export const site = derived(page, $page => $page.data.site)

(incidentally, be sure to define the type for your common PageData in the /src/app.d.ts file)

We no longer need any code in the /+layout.svelte file, the store will be set whenever the page changes.

And oh dear, that’s the problem. Even though our site config data is likely static our store will be triggering updates every time the page store changes. That means we’d re-calculate our cart prices and discounts on every route change, and might be re-rendering things as a result. Svelte is pretty good are avoiding DOM updates that are unnecessary, but it’s still extra work to do those checks that a more efficient store could avoid.

Adding some reactive console.log statements shows that the site store itself and a derived cart store both update whenever page navigation occurs and the page store changes, even though the data we’re interested in hasn’t.

unwanted store updates trigger inefficient store re-calcs and re-renders

De-duplicating derived store wrapper

Thanks to a recent tweak, SvelteKit now only updates the page data that has changed, so we just need to tweak our derived store to de-duplicate the changes as the page store itself updates.

I’ve found the easiest way to do this is by creating a re-usable wrapper store that’s just a few lines of code:

import { derived, type Readable } from 'svelte/store'

// dedupe updates so our store only notifies when changes happen
export function dedupe<T>(store: Readable<T>): Readable<T> {
  let previous: T

  return derived(store, ($value, set) => {
    if ($value !== previous) {
      previous = $value
      set($value)
    }
  })
}

To use it, just wrap the existing derived store we created with this function:

import { derived } from 'svelte/store'
import { page } from '$app/stores'
import { dedupe } from './dedupe'

export const site = dedupe(derived(page, $page => $page.data.site))

Now, we have a read-only store that only triggers updates and re-renders when the reference to the data it returns changes.

The important data update mechanism is all in one place, our layout file is cleaner, and our other stores and components won’t be re-evaluated as often.

Now, our site store only updates when the data actually changes and as a result, so do any derived stores based on it:

deduplicated store updates prevent re-calcs and re-renders

Hope this helps. Let me know what you think in the comments below.