如果是有在寫 React 的讀者多多少少都會聽過 React Query 這個 library ,它在 v4 之後就改名為 TanStack Query 並且開始支援其他框架。
使用前請先安裝
pnpm add @tanstack/svelte-query
# yarn add @tanstack/svelte-query
# npm install @tanstack/svelte-query
先來簡單介紹 TanStack Query,它是一個「管理非同步狀態」的 library ,以 query 來說我通常會是給他一個 queryFn
後會去執行那個 async function ,接著把 response 放進一個「狀態」裡,接下來就可以從那個狀態拿出 data
、 isSuccess
、 isPending
等等幫助我們管理非同步狀態的事情。
在使用前會需要在 src/routes/+layout.svelte
加上:
<script>
import { browser } from '$app/environment';
import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query';
import '../app.css';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
enabled: browser
}
}
});
</script>
<QueryClientProvider client={queryClient}>
<slot />
</QueryClientProvider>
然後我們就能在 +page.svelte
中使用
<script lang="ts">
import { createQuery } from '@tanstack/svelte-query';
interface Todo {
id: number;
title: string;
completed: boolean;
userId: number;
}
const fetchData = async (): Promise<Todo[]> => {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const data = await response.json();
return data;
};
const query = createQuery<Todo[]>({
queryKey: ['queryKey'],
queryFn: async () => await fetchData()
});
</script>
<div class="p-12">
{#if $query.isPending}
Loading...
{/if}
{#if $query.isError}
Error: {$query.error.message}
{/if}
{#if $query.isSuccess}
<div class="flex items-center flex-col gap-4">
{#each $query.data as todo}
<div class="card bg-primary text-primary-content w-96">
<div class="card-body">
<h2 class="card-title">{todo.title}</h2>
<p>{todo.completed ? 'Completed' : 'Not Completed'}</p>
</div>
</div>
{/each}
</div>
{/if}
</div>
而在 @tanstack/svelte-query
中的那個「狀態」是用 Svelte 的 store
來實作,但因為 Svelte 5 裡有它的替代品所以就剛好沒講到了,簡單來說如果我們要使用或者該說訂閱某個 store
,就直接加一個 $
變成 $store
,這樣子當 store
有變化時我們也能拿到最新的值。
那你可能會想那為什麼不用 {#await}
就好?
{#await fetchData()}
Loading...
{:then data}
<div class="flex items-center flex-col gap-4">
{#each data as todo}
<div class="card bg-primary text-primary-content w-96">
<div class="card-body">
<h2 class="card-title">{todo.title}</h2>
<p>{todo.completed ? 'Completed' : 'Not Completed'}</p>
</div>
</div>
{/each}
</div>
{:catch error}
Error: {error.message}
{/await}
當然如果需求只是在 Day 3 時說到的「在 component mounted時打一次 api 然後渲染」這種情況,的確用 {#await}
會簡單很多。
但是如果你想要「手動 refetch」、「將 API result 的資料拿出來做為他用」或者「其他地方需要因應 query 的 status 而有其他行為」之類的需求,使用 {#await}
就會顯得有那麼一點麻煩,你可能需要額外宣告一些 $state
來達成這些需求。
再加上 createQuery
有其他方便的功能,像是 auto refetch 、cache 的控制
畢竟 @tanstack/svelte-query
還算是比較新的 library 且 Svelte 5 還沒正式發布,所以在整合起來還是有一些地方需要注意。
像是如果 createQuery
的某些值來執行需要依靠 $state
來寫的話還是沒辦法正常運行的,
如果我想要做一個 button 來控制什麼時候才要開始進行 fetching
利用 rune 的寫法會發現沒辦法起作用
// ❌ 不要這樣寫
let enabled = $state(false);
let query = $derived(
createQuery<Todo[]>({
queryKey: ['queryKey'],
enabled,
queryFn: async () => await fetchData()
})
);
$effect(() => {
console.log('status', $query.status);
});
雖然我自己覺得的原因是目前 v5 的 reactivity 系統與 v4 store 整合起來還是有點割裂感
必須改為使用 v4 的 store 的方式來撰寫
let enabled = writable(false);
let query = createQuery<Todo[]>(
derived(enabled, (enabled) => ({
queryKey: ['queryKey'],
enabled,
queryFn: async () => await fetchData()
}))
);
$effect(() => {
console.log('data', enabled);
console.log('status', $query.status);
});
<button
class="btn btn-primary"
onclick={() => {
enabled.update((prev) => !prev);
}}
>
toggle
</button>
這或許是這個 library 中目前比較困擾人的點吧QQ
https://github.com/toddLiao469469/30days-for-svelte5/blob/main/src/routes/day14/%2Bpage.svelte