Back to list
exceptionless

tanstack-query

by exceptionless

Exceptionless application

2,449🍴 513📅 Jan 22, 2026

SKILL.md


name: TanStack Query description: | Data fetching and caching with TanStack Query in Svelte. Query patterns, mutations, cache invalidation, WebSocket-driven updates, and optimistic updates. Keywords: createQuery, createMutation, TanStack Query, query keys, cache invalidation, optimistic updates, refetch, stale time, @exceptionless/fetchclient, WebSocket

TanStack Query

Documentation: tanstack.com/query | Use context7 for API reference

Centralize API calls in api.svelte.ts per feature using TanStack Query with @exceptionless/fetchclient.

Query Basics

// src/lib/features/organizations/api.svelte.ts
import { createQuery, createMutation, useQueryClient } from '@tanstack/svelte-query';
import { useFetchClient, type ProblemDetails } from '@exceptionless/fetchclient';

export function getOrganizationsQuery() {
    const client = useFetchClient();

    return createQuery(() => ({
        queryKey: ['organizations'],
        queryFn: async () => {
            const response = await client.getJSON<Organization[]>('/organizations');
            if (!response.ok) {
                throw response.problem;
            }
            return response.data!;
        }
    }));
}

Query Keys Convention

Use a queryKeys factory per feature for type safety and consistency:

// From src/lib/features/webhooks/api.svelte.ts
export const queryKeys = {
    type: ['Webhook'] as const,
    id: (id: string | undefined) => [...queryKeys.type, id] as const,
    ids: (ids: string[] | undefined) => [...queryKeys.type, ...(ids ?? [])] as const,
    project: (id: string | undefined) => [...queryKeys.type, 'project', id] as const,
    deleteWebhook: (ids: string[] | undefined) => [...queryKeys.ids(ids), 'delete'] as const,
    postWebhook: () => [...queryKeys.type, 'post'] as const
};

Common patterns:

// Resource list
['organizations']
['projects']

// Single resource
['organizations', organizationId]
['projects', projectId]

// Nested resources
['organizations', organizationId, 'projects']
['projects', projectId, 'events']

// Filtered queries
['events', { projectId, status: 'open' }]

Using Queries in Components

<script lang="ts">
    import { getOrganizationsQuery } from '$features/organizations/api.svelte';

    const organizationsQuery = getOrganizationsQuery();
</script>

{#if organizationsQuery.isPending}
    <LoadingSpinner />
{:else if organizationsQuery.isError}
    <ErrorMessage error={organizationsQuery.error} />
{:else}
    {#each organizationsQuery.data as org}
        <OrganizationCard {org} />
    {/each}
{/if}

Mutations

export function createOrganizationMutation() {
    const client = useFetchClient();
    const queryClient = useQueryClient();

    return createMutation(() => ({
        mutationFn: async (data: CreateOrganizationRequest) => {
            const response = await client.postJSON<Organization>('/organizations', data);
            if (!response.ok) {
                throw response.problem;
            }
            return response.data!;
        },
        onSuccess: () => {
            // Invalidate and refetch organizations list
            queryClient.invalidateQueries({ queryKey: ['organizations'] });
        }
    }));
}

Using Mutations

<script lang="ts">
    import { createOrganizationMutation } from '$features/organizations/api.svelte';

    const createMutation = createOrganizationMutation();

    async function handleCreate(data: CreateOrganizationRequest) {
        try {
            const org = await createMutation.mutateAsync(data);
            goto(`/organizations/${org.id}`);
        } catch (error) {
            // Error handled by form or toast
        }
    }
</script>

<Button
    onclick={() => handleCreate(formData)}
    disabled={createMutation.isPending}
>
    {createMutation.isPending ? 'Creating...' : 'Create'}
</Button>

Naming Conventions

Functions follow HTTP verb prefixes:

// Queries (GET)
export function getOrganizationsQuery() { ... }
export function getOrganizationQuery(id: string) { ... }
export function getProjectEventsQuery(projectId: string) { ... }

// Mutations
export function postOrganizationMutation() { ... }  // CREATE
export function patchOrganizationMutation() { ... } // UPDATE
export function deleteOrganizationMutation() { ... } // DELETE

Dependent Queries

export function getProjectQuery(projectId: string) {
    const client = useFetchClient();

    return createQuery(() => ({
        queryKey: ['projects', projectId],
        queryFn: async () => {
            const response = await client.getJSON<Project>(`/projects/${projectId}`);
            if (!response.ok) throw response.problem;
            return response.data!;
        },
        enabled: !!projectId // Only run when projectId is truthy
    }));
}

Optimistic Updates

export function updateOrganizationMutation() {
    const client = useFetchClient();
    const queryClient = useQueryClient();

    return createMutation(() => ({
        mutationFn: async ({ id, data }: { id: string; data: UpdateOrganizationRequest }) => {
            const response = await client.patchJSON<Organization>(`/organizations/${id}`, data);
            if (!response.ok) throw response.problem;
            return response.data!;
        },
        onMutate: async ({ id, data }) => {
            // Cancel in-flight queries
            await queryClient.cancelQueries({ queryKey: ['organizations', id] });

            // Snapshot previous value
            const previous = queryClient.getQueryData<Organization>(['organizations', id]);

            // Optimistically update
            queryClient.setQueryData(['organizations', id], (old: Organization) => ({
                ...old,
                ...data
            }));

            return { previous };
        },
        onError: (err, variables, context) => {
            // Rollback on error
            if (context?.previous) {
                queryClient.setQueryData(['organizations', variables.id], context.previous);
            }
        },
        onSettled: (data, error, { id }) => {
            // Always refetch after mutation
            queryClient.invalidateQueries({ queryKey: ['organizations', id] });
        }
    }));
}

Prefetching

export function prefetchOrganization(id: string) {
    const client = useFetchClient();
    const queryClient = useQueryClient();

    return queryClient.prefetchQuery({
        queryKey: ['organizations', id],
        queryFn: async () => {
            const response = await client.getJSON<Organization>(`/organizations/${id}`);
            if (!response.ok) throw response.problem;
            return response.data!;
        }
    });
}

WebSocket-Driven Invalidation

Invalidate queries when WebSocket messages arrive:

// From src/lib/features/webhooks/api.svelte.ts
import type { WebSocketMessageValue } from '$features/websockets/models';
import { QueryClient } from '@tanstack/svelte-query';

export async function invalidateWebhookQueries(
    queryClient: QueryClient,
    message: WebSocketMessageValue<'WebhookChanged'>
) {
    const { id, organization_id, project_id } = message;

    if (id) {
        await queryClient.invalidateQueries({ queryKey: queryKeys.id(id) });
    }

    if (project_id) {
        await queryClient.invalidateQueries({ queryKey: queryKeys.project(project_id) });
    }

    // Fallback: invalidate all if no specific keys
    if (!id && !organization_id && !project_id) {
        await queryClient.invalidateQueries({ queryKey: queryKeys.type });
    }
}

Wire up in WebSocket handler:

// In WebSocket message handler
onMessage('WebhookChanged', (message) => {
    invalidateWebhookQueries(queryClient, message);
});

Score

Total Score

80/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 1000以上

+15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

+5
Issue管理

オープンIssueが50未満

0/5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon