
Caching Data in SvelteKit
This post is all about data handling. We’ll add some rudimentary search functionality that will modify the page’s query string (using built-in SvelteKit features), and re-trigger the page’s loader. But, rather than just re-query our (imaginary) database, we’ll add some caching so re-searching prior searches (or using the back button) will show previously retrieved data, quickly, from cache. We’ll look at how to control the length of time the cached data stays valid and, more importantly, how to manually invalidate all cached values. As icing on the cake, we’ll look at how we can manually update the data on the current screen, client-side, after a mutation, while still purging the cache.
Unfortunately, the web platform’s features are a bit lower level, so we’ll be doing a bit more work than you might be used to. The upside is we won’t need any external libraries, which will help keep bundle sizes nice and small. Please don’t use the approaches I’m going to show you unless you have a good reason to. Caching is easy to get wrong, and as you’ll see, there’s a bit of complexity that’ll result in your application code. Hopefully your data store is fast, and your UI is fine allowing SvelteKit to just always request the data it needs for any given page. If it is, leave it alone. Enjoy the simplicity. But this post will show you some tricks for when that stops being the case.
Now let’s add a simple form to our /list
page:
Yep, forms can target directly to our normal page loaders. Now we can add a search term in the search box, hit Enter, and a “search” term will be appended to the URL’s query string, which will re-run our loader and search our to-do items.
Let’s also increase the delay in our todoData.js
file in /lib/data
. This will make it easy to see when data are and are not cached as we work through this post.
Let’s get started by adding some caching to our /api/todos
endpoint. We’ll go back to our +server.js
file and add our first cache-control header.
…which will leave the whole function looking like this:
And just like that, our queries are caching.
Note make sure you un-check the checkbox that disables caching in dev tools.
Remember, if your initial navigation on the app is the list page, those search results will be cached internally to SvelteKit, so don’t expect to see anything in DevTools when returning to that search.
After that initial load, when you start searching on the page, you should see network requests from your browser to the /api/todos
list. As you search for things you’ve already searched for (within the last 60 seconds), the responses should load immediately since they’re cached.
What’s especially cool with this approach is that, since this is caching via the browser’s native caching, these calls could (depending on how you manage the cache busting we’ll be looking at) continue to cache even if you reload the page (unlike the initial server-side load, which always calls the endpoint fresh, even if it did it within the last 60 seconds).
Obviously data can change anytime, so we need a way to purge this cache manually, which we’ll look at next.
Right now, data will be cached for 60 seconds. No matter what, after a minute, fresh data will be pulled from our datastore. You might want a shorter or longer time period, but what happens if you mutate some data and want to clear your cache immediately so your next query will be up to date? We’ll solve this by adding a query-busting value to the URL we send to our new /todos
endpoint.
Let’s store this cache busting value in a cookie. That value can be set on the server but still read on the client. Let’s look at some sample code.
We can create a +layout.server.js
file at the very root of our routes
folder. This will run on application startup, and is a perfect place to set an initial cookie value.
You may have noticed the isDataRequest
value. Remember, layouts will re-run anytime client code calls invalidate()
, or anytime we run a server action (assuming we don’t turn off default behavior). isDataRequest
indicates those re-runs, and so we only set the cookie if that’s false
; otherwise, we send along what’s already there.
The httpOnly: false
flag is also significant. This allows our client code to read these cookie values in document.cookie
. This would normally be a security concern, but in our case these are meaningless numbers that allow us to cache or cache bust.
Our universal loader is what calls our /todos
endpoint. This runs on the server or the client, and we need to read that cache value we just set up no matter where we are. It’s relatively easy if we’re on the server: we can call await parent()
to get the data from parent layouts. But on the client, we’ll need to use some gross code to parse document.cookie
:
Fortunately, we only need it once.
But now we need to send this value to our /todos
endpoint.
getCurrentCookieValue('todos-cache')
has a check in it to see if we’re on the client (by checking the type of document), and returns nothing if we are, at which point we know we’re on the server. Then it uses the value from our layout.
But how do we actually update that cache busting value when we need to? Since it’s stored in a cookie, we can call it like this from any server action:
It’s all downhill from here; we’ve done the hard work. We’ve covered the various web platform primitives we need, as well as where they go. Now let’s have some fun and write application code to tie it all together.
For reasons that’ll become clear in a bit, let’s start by adding an editing functionality to our /list
page. We’ll add this second table row for each todo:
And, of course, we’ll need to add a form action for our /list
page. Actions can only go in .server
pages, so we’ll add a +page.server.js
in our /list
folder. (Yes, a +page.server.js
file can co-exist next to a +page.js
file.)
We’re grabbing the form data, forcing a delay, updating our todo, and then, most importantly, clearing our cache bust cookie.
Let’s give this a shot. Reload your page, then edit one of the to-do items. You should see the table value update after a moment. If you look in the Network tab in DevToold, you’ll see a fetch to the /todos
endpoint, which returns your new data. Simple, and works by default.
What if we want to avoid that fetch that happens after we update our to-do item, and instead, update the modified item right on the screen?
This isn’t just a matter of performance. If you search for “post” and then remove the word “post” from any of the to-do items in the list, they’ll vanish from the list after the edit since they’re no longer in that page’s search results. You could make the UX better with some tasteful animation for the exiting to-do, but let’s say we wanted to not re-run that page’s load function but still clear the cache and update the modified to-do so the user can see the edit. SvelteKit makes that possible — let’s see how!
Before, we were accessing our to-dos on the data
prop, which we do not own and cannot update. But Svelte lets us return our data in their own store (assuming we’re using a universal loader, which we are). We just need to make one more tweak to our /list
page.
Instead of this:
…we need to do this since todos
is itself now a store.:
Now our data loads as before. But since todos
is a writeable store, we can update it.
First, let’s provide a function to our use:enhance
attribute:
This will run before a submit. Let’s write that next:
We now call update
on our todos
array since it’s a store. And that’s that. After editing a to-do item, our changes show up immediately and our cache is cleared (as before, since we set a new cookie value in our editTodo
form action). So, if we search and then navigate back to this page, we’ll get fresh data from our loader, which will correctly exclude any updated to-do items that were updated.
We can set cookies in any server load function (or server action), not just the root layout. So, if some data are only used underneath a single layout, or even a single page, you could set that cookie value there. Moreoever, if you’re not doing the trick I just showed manually updating on-screen data, and instead want your loader to re-run after a mutation, then you could always set a new cookie value right in that load function without any check against isDataRequest
. It’ll set initially, and then anytime you run a server action that page layout will automatically invalidate and re-call your loader, re-setting the cache bust string before your universal loader is called.
Let’s wrap-up by building one last feature: a reload button. Let’s give users a button that will clear cache and then reload the current query.
We’ll add a dirt simple form action:
In a real project you probably wouldn’t copy/paste the same code to set the same cookie in the same way in multiple places, but for this post we’ll optimize for simplicity and readability.
Now let’s create a form to post to it:
That works!
We could call this done and move on, but let’s improve this solution a bit. Specifically, let’s provide feedback on the page to tell the user the reload is happening. Also, by default, SvelteKit actions invalidate everything. Every layout, page, etc. in the current page’s hierarchy would reload. There might be some data that’s loaded once in the root layout that we don’t need to invalidate or re-load.
So, let’s focus things a bit, and only reload our to-dos when we call this function.
First, let’s pass a function to enhance:
We’re setting a new reloading
variable to true
at the start of this action. And then, in order to override the default behavior of invalidating everything, we return an async
function. This function will run when our server action is finished (which just sets a new cookie).
Without this async
function returned, SvelteKit would invalidate everything. Since we’re providing this function, it will invalidate nothing, so it’s up to us to tell it what to reload. We do this with the invalidate
function. We call it with a value of reload:todos
. This function returns a promise, which resolves when the invalidation is complete, at which point we set reloading
back to false
.
Lastly, we need to sync our loader up with this new reload:todos
invalidation value. We do that in our loader with the depends
function:
And that’s that. depends
and invalidate
are incredibly useful functions. What’s cool is that invalidate
doesn’t just take arbitrary values we provide like we did. We can also provide a URL, which SvelteKit will track, and invalidate any loaders that depend on that URL. To that end, if you’re wondering whether we could skip the call to depends
and invalidate our /api/todos
endpoint altogether, you can, but you have to provide the exact URL, including the search
term (and our cache value). So, you could either put together the URL for the current search, or match on the path name, like this:
This was a long post, but hopefully not overwhelming. We dove into various ways we can cache data when using SvelteKit. Much of this was just a matter of using web platform primitives to add the correct cache, and cookie values, knowledge of which will serve you in web development in general, beyond just SvelteKit.
Moreover, this is something you absolutely do not need all the time. Arguably, you should only reach for these sort of advanced features when you actually need them. If your datastore is serving up data quickly and efficiently, and you’re not dealing with any kind of scaling problems, there’s no sense in bloating your application code with needless complexity doing the things we talked about here.
As always, write clear, clean, simple code, and optimize when necessary. The purpose of this post was to provide you those optimization tools for when you truly need them. I hope you enjoyed it!
If you need help creating a digital marketing strategy for your business, don’t hesitate to contact one of Digidude’s consultants.
Post a Comment
You must be logged in to post a comment.