Intro to Svelte Query
Svelte is an exciting frontend framework that compiles away, which results in very small bundles shipped to the client with rich application functionality and features. In this post, I want to show case a new module that is being ported from React, called Svelte Query. Svelte Query is a module that gives you control of your server requests by managing all of the side effect edge cases for you.
Tutorial
Let's create a demo Svelte App that uses Svelte Query to review movies, the app will search and list movies using pagination and allow the user to add movie reviews for movies they watched.
npx degit sveltejs/template query-demo
cd query-demo
yarn
yarn add -D tinro
yarn add @sveltestack/svelte-query
Setup Svelte-Query Provider
In App.svelte
<script>
import { QueryClient, QueryClientProvider } from '@sveltestack/svelte-query'
import { Route } from 'tinro'
import Users from './Users.svelte'
const queryClient = new QueryClient()
</script>
<QueryClientProvider client={queryClient}>
<Route path="/">
<h1>Svelte Query Demo</h1>
<div>
<a href="/users">Users</a>
</div>
</Route>
<Route path="/users">
<Users />
</Route>
</QueryClientProvider>
Basics
We are using https://jsonplaceholder.typicode.com as our API service.
Create a new file src/Users.svelte
<script>
import {useQuery} from '@sveltestack/svelte-query'
const usersUrl = 'https://jsonplaceholder.typicode.com/users'
const queryResult = useQuery('users', () =>
fetch(usersUrl).then(res => res.json())
)
</script>
<header>
<a href="/">Home</a>
<h1>Users</h1>
</header>
{#if $queryResult.isLoading}
<span>Loading...</span>
{:else if $queryResult.error}
<span>Error</span>
{:else}
<div>
<ul>
{#each $queryResult.data as user}
<li>{user.username} - {user.email}</li>
{/each}
</ul>
</div>
{/if}
You will notice, we are using the {#if}
command to check for the status of the query and display the appropriate markup. When the query is successful, we use the data property with the {#each}
command to loop over every result and render the markup to the screen. The useQuery hook
accepts any function that returns a promise.
Pagination
One of the many benefits of the Svelte Query component, is the ability to do pagination. Let's take a look:
Create a src/Posts.svelte
file
touch src/Posts.svelte
<script>
import { Query } from '@sveltestack/svelte-query'
let page = 1
let posts = []
</script>
<header>
<h1>Posts</h1>
</header>
{#each posts as post}
{/each}
<span>Current Page: {page}</span>
<div>
<button on:click={() => null}>Previous</button>
<button on:click={() => null}>Next</button>
</div>
Modify the App.svelte
file to include the Posts.svelte file
<script>
import { QueryClient, QueryClientProvider } from '@sveltestack/svelte-query'
import { Route } from 'tinro'
import Users from './Users.svelte'
import Posts from './Posts.svelte'
const queryClient = new QueryClient()
</script>
<QueryClientProvider client={queryClient}>
<Route path="/">
<h1>Svelte Query Demo</h1>
<div>
<a href="/users">Users</a>
<a href="/posts">Posts</a>
</div>
</Route>
<Route path="/users">
<Users />
</Route>
<Route path="/posts">
<Posts />
</Route>
</QueryClientProvider>
Now in the Posts.svelte
file, lets build our paginated query
<script>
import { Query } from '@sveltestack/svelte-query'
let page = 1
const fetchPosts = (page = 1) => fetch(
`https://jsonplaceholder.typicode.com/posts?_page=${page}`
).then(res => res.json())
$: queryOptions = {
queryKey: ['posts', page],
queryFn: () => fetchPosts(page),
keepPreviousData: true
}
</script>
<header>
<a href="/">Home</a>
<h1>Posts</h1>
</header>
<Query options={queryOptions}>
<div slot="query" let:queryResult>
{#if queryResult.status === 'loading'}
Loading...
{:else if queryResult.status === 'error'}
<span>Error: {queryResult.error.message}</span>
{:else}
{#each queryResult.data as post}
<p>{post.title}</p>
{/each}
{/if}
</div>
</Query>
<span>Current Page: {page}</span>
<div>
<button disabled={page === 1} on:click={() => {
if (page > 1) {
page = page - 1
}
}}>Previous</button>
<button on:click={() => page = page + 1}>Next</button>
</div>
In the script section we add our fetchPosts
function, and we use the reactive symbol to basically watch the page variable. Every time the page variable changes we will get a new queryOptions object.
In the markup section, we add a Query
component and set the prop options
to our queryOptions
object. The Query component uses slots, so we provide a child div with the slot named "query" and set a let directive to assign the queryResult variable so that we can access it within our slot implementation.
Finally, we modify the buttons below to increment and decrement the page variable, and BAM! ⚡ We have pagination! ⚡
Mutation
Not only do we need to list items, we need to send data to our services, with Svelte Query, we do this with a mutation hook.
Lets create a new file called src/Todos.svelte
<script>
import { useQuery, useMutation } from '@sveltestack/svelte-query'
const url = 'https://jsonplaceholder.typicode.com/todos'
</script>
<header>
<a href="/">Home</a>
<h1>Todos</h1>
</header>
<form>
<input type="text" name="title" placeholder="todo" />
<button type="submit">Submit</button>
</form>
<ul>
</ul>
And lets add the Todos component to the App.svelte file
<script>
import { QueryClient, QueryClientProvider } from '@sveltestack/svelte-query'
import { Route } from 'tinro'
import Users from './Users.svelte'
import Todos from './Todos.svelte'
import Posts from './Posts.svelte'
const queryClient = new QueryClient()
</script>
<QueryClientProvider client={queryClient}>
<Route path="/">
<h1>Svelte Query Demo</h1>
<div>
<a href="/users">Users</a>
<a href="/todos">Todos</a>
<a href="/posts">Posts</a>
</div>
</Route>
<Route path="/users">
<Users />
</Route>
<Route path="/todos">
<Todos />
</Route>
<Route path="/posts">
<Posts />
</Route>
</QueryClientProvider>
Open the Todos.svelte
and lets create add our query and mutation
<script>
import { useMutation } from '@sveltestack/svelte-query'
const url = 'https://jsonplaceholder.typicode.com/todos'
let title = ''
const mutation = useMutation(newTodo =>
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTodo)
}).then(res => res.json())
const onSubmit = (e) => {
$mutation.mutate({ userId: 1, title })
title = ''
}
</script>
<header>
<a href="/">Home</a>
<h1>Todos</h1>
</header>
{#if $mutation.isLoading}
<p>Adding todo...</p>
{:else if $mutation.isError}
<p>Error: {$mutation.error.message}</p>
{:else if $mutation.isSuccess}
<p>Success!</p>
{:else}
<form on:submit|preventDefault={onSubmit}>
<input bind:value={title} type="text" name="title" placeholder="todo" />
<button type="submit">Submit</button>
</form>
{/if}
We create our useMutation hook and give it a function that takes a todo object and returns a promise. Then when we call the mutate function on form submit, we can manage the states using the mutation store.