My previous post provided a general overview of SvelteKit and its usefulness in web development. In this post, we will delve into the topic of caching, which is of interest to many developers. If you haven’t read my previous post, I encourage you to do so. You can find the code for this post on GitHub, along with a live demo.
The focus of this post is data handling. We will implement basic search functionality that modifies the query string of the page and triggers the loader. Rather than re-querying our database, we will incorporate caching to quickly retrieve previously searched data from the cache. We will explore how to control the duration of cached data and manually invalidate cached values. Additionally, we will learn how to manually update the data on the current screen after a mutation, while still purging the cache.
This post will be more complex than my usual posts, as we will cover advanced topics. I will demonstrate how to implement common features found in popular data utilities like react-query, using only SvelteKit features and the web platform. Although the web platform’s features require more work, not relying on external libraries will help keep bundle sizes small. However, I advise against using these approaches unless necessary, as caching can be tricky to get right and introduces complexity to your application code. If your data store is fast and your UI allows SvelteKit to always request the required data, it is best to stick with the simplicity of that approach. But if you encounter situations where that isn’t feasible, this post will provide some useful tricks.
On a side note, react-query has recently been released for Svelte! If you frequently rely on manual caching techniques, I recommend checking out that project to see if it can assist you.
Before we begin, let’s make a few small changes to the code from our previous post. This will allow us to explore additional SvelteKit features and set ourselves up for success. Firstly, let’s move the data loading from our loader in +page.server.js to an API route. We will create a +server.js file in the routes/api/todos directory and add a GET function. This change will allow us to fetch data from the /api/todos path using the default GET verb. The code for data loading remains the same.
Next, let’s rename our page loader file from +page.server.js to +page.js (or .ts if you are using TypeScript). This modification transforms our loader into a “universal” loader that runs on both the server and the client. One advantage of this change is that the fetch call to our new endpoint will be executed in the browser using the native fetch function. We will add standard HTTP caching later, but for now, we will make a simple call to the endpoint.
Now, let’s add a basic search form to our /list page using HTML tags. This form will allow users to enter a search term, which will be appended to the URL’s query string when they hit Enter. This will trigger the loader and search for to-do items accordingly.
To make it easier to observe when data is or isn’t cached, let’s increase the delay in our todoData.js file in the /lib/data directory.
With these changes made, we can start implementing caching in our /api/todos endpoint. In the +server.js file, we will add a cache-control header to enable caching for this endpoint. We will set the cache-control header to “max-age=60”, which indicates that responses should be cached for 60 seconds. You can adjust this value based on your specific requirements. Additionally, remember to uncheck the checkbox that disables caching in the browser’s developer tools.
Once caching is implemented, the initial load of our app (assuming we start at the /list page) will be fetched on the server. SvelteKit will send this data down to the client and observe the Cache-Control header to determine if it can use the cached data within the defined cache window. Subsequent searches on the page will trigger network requests from the client to the /api/todos endpoint. If the searched data was requested within the last 60 seconds, the response will load immediately from the cache. An interesting aspect of this approach is that the browser’s native caching allows the calls to continue to cache even if the page is reloaded, unlike the initial server-side load, which always fetches fresh data within the cache window. Keep in mind that data can change at any time, so we need a way to manually purge the cache, which we will explore next.
Currently, data is cached for 60 seconds before fresh data is fetched. However, what if you mutate some data and want to immediately clear the cache to ensure your next query is up to date? We can solve this by adding a query-busting value to the URL sent to our /todos endpoint and storing this value in a cookie. The cookie can be set on the server but still read on the client. Here is a sample code snippet that demonstrates this approach:
In the +layout.server.js file, which is located at the root of our routes folder, we can set an initial cookie value. This layout file runs on application startup and is an ideal place to set the cookie. We use the isDataRequest variable to determine if the layout is being re-run due to a call to invalidate() or a server action. We only set the cookie if isDataRequest is false, indicating that the layout is not being re-run. The cookie value can be accessed in the client-side code using document.cookie. The httpOnly: false flag ensures that the client can read the cookie values.
That covers the basic concepts of caching and cache invalidation in SvelteKit. Remember to adjust the cache-control header and cache busting mechanisms based on your specific requirements. Feel free to explore the full code for this post on GitHub if you need more details.