State in URL: the SvelteKit approach
Resources
GitHub repository: https://github.com/JustinyAhin/okupter-repos/tree/main/apps/sveltekit-state-in-url
SvelteKit version: 1.16.2
Some context
Last week, while working on a project for the WNBA at my day job, I came across an interesting task. We were building a component that would display some data fetched on the server side. On the frontend, we would have a dropdown that would allow the user to filter the data by a certain property.
The data we were fetching was big enough that we didn’t want to fetch it all at once. Instead, we only fetch on page load a part of it and update the UI every time the user changes the filter.
We were working with Next.js, so I used getServerSideProps to fetch the data and some states to keep track of the current filter. To update the data on filter change, I used Next.js router to push a new URL with the new filter as a query parameter. This triggered a new server-side render, and the component was updated with the new data.
While working on this component, I wondered how to do the same thing with SvelteKit.
This blog post is a summary of the approach I’d use to build the same component in SvelteKit.
What I’m going to build
To illustrate the approach, I’m going to build a simple component that displays a list of countries and allows the user to filter them by continent.
I’m using the restcountries API to get the data.
Getting the data on the server
To fetch our data from the server, we're going to use SvelteKit load function. This function is first called on the server on the initial page load and then on the client when navigating to the page.
Since we need the data returned to be updated every time the user changes the filter, we need to "make it depends" on whatever we trigger on the client when the user changes the filter. In our case, we're going to use the URL query parameters.
// src/routes/+page.server.ts
// Import statements
import type { PageServerLoad } from './$types';
import type { Country } from '$lib/types';
// First let's create a function that fetches the data from the API
// The function will take a 'region' parameter, and return the list of countries
const API_URL = 'https://restcountries.com/v3.1';
const getCountriesByRegion = async (region: string) => {
const response = await fetch(`${API_URL}/region/${region}`);
const countries = (await response.json()) as Country[];
return countries;
};
// We can now export our load function
export const load = (async ({ url }) => {
// Per default, only fetch the countries from the African continent
const region = url.searchParams.get('region') || 'africa';
const countries = await getCountriesByRegion(region);
return {
countries
};
}) satisfies PageServerLoad;Our load function is pretty simple. It fetches the data from the API and returns it as a prop to the component.
We could have made it a bit more complete by adding some validation in case the user manually enters an invalid region in the URL. Think of something like:
const REGIONS_SLUGS = ['africa', 'americas', 'asia', 'europe', 'oceania'];
// And then we can make sure the region in the query params is validDisplaying the data
Data table
Since we are returning the data from the server as a prop, we can now display it in our component.
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
</script>
<section>
{#if data.countries.length > 0}
<table>
<thead>
<tr>
<th>Flag</th>
<th>Name</th>
<th>Capital</th>
</tr>
</thead>
<tbody>
{#each data.countries as country}
<tr>
<td><img src={country.flags.svg} alt={country.name.official} class="flag" /></td>
<td>{country.name.official}</td>
<td>{country.capital}</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</section>Quite straightforward. We just iterate over the list of countries and display them in a table.
Using directly data.countries might be a bit verbose, but it spares us from having to create another reactive variable to keep track of the countries.
The select dropdown
Now that we have our data displayed let's add a dropdown to filter the countries by continent. This is where the fun begins.
<script lang="ts">
// Import statements
import { goto } from '$app/navigation';
import { page } from '$app/stores';
// We create a list of all continents with their slug and name
const REGIONS = [
{
slug: 'africa',
name: 'Africa'
},
{
slug: 'americas',
name: 'Americas'
},
{
slug: 'asia',
name: 'Asia'
},
{
slug: 'europe',
name: 'Europe'
},
{
slug: 'oceania',
name: 'Oceania'
}
];
// We create a reactive variable to keep track of the current region
// It will be equal to the value for 'region' in the URL query params if any
// or 'africa' by default
let selectedRegion = $page.url.searchParams.get('region') || REGIONS[0].slug;
// Now we create a function that will be called every time the user changes the region
const handleRegionChange = () => {
goto(`?region=${selectedRegion}`);
};
</script>The important part here is the handleRegionChange function. It uses the goto function from SvelteKit to push a new URL with the new region as a query parameter. This is all we need to trigger a new server-side render and update the data.
Now, we can populate the select dropdown with the list of regions we created earlier and bind its value to the selectedRegion variable.
<section>
<div>
<form>
<select bind:value={selectedRegion} on:change={handleRegionChange}>
{#each REGIONS as region, index}
<option value={region.slug}>{region.name}</option>
{/each}
</select>
</form>
</div>
</section>And that's all we need.
Every time the user changes the region, the server will fetch the data from the API again, and the component will update. The reason why the server can re-fetch the data is because of how SvelteKit handles invalidation. If your load function depends on or references a property whose value has changed, the page will be invalidated, and the load function will be called again.
Conclusion
I like how SvelteKit makes it easy to handle reactive and stateful data. The approach is clean, simple, and very powerful.