npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

svelte-simple-query

v0.0.6

Published

A simple yet powerful, lightweight data query library for Svelte 5, providing full control with built-in functionalities. Built with TypeScript for easy usage and strong typing.

Downloads

187

Readme

Svelte Simple Query

A simple yet powerful, lightweight data query library for Svelte 5, providing full control with built-in functionalities. Built with TypeScript for easy usage and strong typing.

Features

  • TypeScript Support: Fully typed for better development experience.
  • Query Management: Simple and flexible querying system.
  • Data Management: Supports fetching, caching, and mutations.
  • Dynamic Querying: Fetch dynamic endpoints effortlessly.
  • Automatic Retries: Built-in retry logic on failure.
  • Cache Management: Control cache behavior with timeout and group management.
  • Error & Loading States: Built-in handling for loading, error, and success states.

Installation

npm install svelte-simple-query

Quick Start

<script lang="ts">
	import { Query, useQuery } from 'svelte-simple-query';

	Query.setup({
		baseURI: 'https://api.example.com'
	});

	interface User {
		id: number;
		name: string;
		email: string;
	}

	let users = useQuery<User[]>('/users');
	users.fetch();
</script>

<div>
	{#if users.isLoading}
		<p>Loading users...</p>
	{:else if users.isError}
		<p class="error">Error: {users.isError}</p>
	{:else if users.data}
		<ul>
			{#each users.data as user (user.id)}
				<li>{user.name} ({user.email})</li>
			{/each}
		</ul>
	{:else}
		<p>No data</p>
	{/if}
</div>

Examples

Featured Examples

  • Minimal - Basic setup and data fetching
  • Basic - Error handling, loading states, and refetching
  • Pagination - Page-based data fetching with navigation

Common Use Cases

<script lang="ts">
	import { useQuery } from 'svelte-simple-query';

	interface Post {
		id: number;
		title: string;
		body: string;
	}

	let posts = useQuery<Post[]>('/posts', {
		cacheTimeout: 5000 // Cache for 5 seconds
	});
	posts.fetch();
</script>

{#if posts.isLoading}
	Loading posts...
{:else if posts.isError}
	Failed to load: {posts.isError}
{:else if posts.data}
	{#each posts.data as post (post.id)}
		<article>
			<h3>{post.title}</h3>
			<p>{post.body}</p>
		</article>
	{/each}
{/if}
<script lang="ts">
	import { useQuery } from 'svelte-simple-query';

	interface User {
		id: number;
		name: string;
	}

	let users = useQuery<User[]>('/users');
	users.fetch();
</script>

{#if users.isError}
	<div class="error">
		<!-- Error can be a string or Error object with status/info -->
		{#if typeof users.isError === 'object' && users.isError.status}
			<p>Error {users.isError.status}</p>
			<p>Details: {JSON.stringify(users.isError.info)}</p>
		{:else}
			<p>{users.isError}</p>
		{/if}
		<button onclick={() => users.refetch()}>Retry</button>
	</div>
{:else if users.isLoading}
	<p>Loading...</p>
{:else if users.data}
	<ul>
		{#each users.data as user (user.id)}
			<li>{user.name}</li>
		{/each}
	</ul>
{/if}
<script lang="ts">
	import { mutate, useQuery, Query } from 'svelte-simple-query';

	interface User {
		id: number;
		name: string;
	}

	const updateUser = async (userId: number, newData: Partial<User>) => {
		try {
			// Step 1: Make server mutation (POST/PUT/DELETE)
			const response = await fetch(`/api/users/${userId}`, {
				method: 'PUT',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify(newData)
			});

			if (!response.ok) throw new Error('Update failed');
			const updatedUser = await response.json();

			// Step 2: Update cache with server response
			await mutate(`/users/${userId}`, {
				data: updatedUser
			});

			return updatedUser;
		} catch (error) {
			console.error('Update failed:', error);
			throw error;
		}
	};

	// Optimistic Update Pattern
	const optimisticUpdate = async (userId: number, newData: Partial<User>, originalData: User) => {
		try {
			// Update cache immediately (optimistic)
			await mutate(`/users/${userId}`, {
				data: { ...originalData, ...newData }
			});

			// Then make server mutation
			const response = await fetch(`/api/users/${userId}`, {
				method: 'PUT',
				body: JSON.stringify(newData)
			});

			if (!response.ok) throw new Error('Update failed');
			const updatedUser = await response.json();

			// Update cache with server response
			await mutate(`/users/${userId}`, { data: updatedUser });
		} catch (error) {
			// Revert cache on error
			await mutate(`/users/${userId}`, { data: originalData });
			console.error('Update failed, reverted:', error);
		}
	};
</script>

<button onclick={() => updateUser(1, { name: 'John Doe' }, originalUser)}> Update User </button>
<script lang="ts">
	import { useDynamicQueries } from 'svelte-simple-query';

	interface Post {
		id: number;
		title: string;
	}

	let postId = $state<number | null>(null);

	// Create multiple queries based on dynamic IDs
	const posts = useDynamicQueries<Post>((id: number) => `/posts/${id}`);

	const loadPost = async (id: number) => {
		postId = id;
		await posts[id].fetch();
	};
</script>

<input
	type="number"
	placeholder="Enter post ID"
	onchange={(e) => loadPost(parseInt(e.target.value))}
/>

{#if postId && posts[postId]?.data}
	<div>
		<h3>{posts[postId].data.title}</h3>
	</div>
{/if}
<script lang="ts">
	import { useQuery, Query } from 'svelte-simple-query';

	// Group related queries
	let usersPageA = useQuery('/users?page=1', { group: 'user-pages' });
	let usersPageB = useQuery('/users?page=2', { group: 'user-pages' });
	usersPageA.fetch();
	usersPageB.fetch();

	const clearAllUserPages = () => {
		Query.clearGroup('user-pages'); // Clear all queries in the group
	};

	const returnGroup = () => {
		const allUserQueries = Query.group('user-pages');
		console.log(allUserQueries);
	};
</script>

<button onclick={clearAllUserPages}>Clear All User Pages</button>
<script lang="ts">
	import { page } from '$app/state';
	import { useQuery } from 'svelte-simple-query';
	import { untrack } from 'svelte';

	interface User {
		id: number;
		name: string;
		email: string;
	}

	// Derive URL params
	const pageNum = $derived(page.url.searchParams.get('page') || '1');
	const sortBy = $derived(page.url.searchParams.get('sort') || 'name');
	const search = $derived(page.url.searchParams.get('search') || '');

	// Query state
	let users = $state(useQuery<User[]>('/users'));

	// Refetch when params change
	$effect(() => {
		pageNum, sortBy, search; // Track dependencies
		untrack(() => {
			const endpoint = `/users?page=${pageNum}&sort=${sortBy}${search ? `&search=${search}` : ''}`;
			users = useQuery<User[]>(endpoint);
			users.fetch();
		});
	});

	// Update URL and let $effect handle refetch
	const updateParams = (key: string, value: string | null) => {
		const params = new URLSearchParams(page.url.search);
		if (value === null) {
			params.delete(key);
		} else {
			params.set(key, value);
		}

		// Reset to page 1 when sorting/searching
		if ((key === 'sort' || key === 'search') && params.get('page') !== '1') {
			params.set('page', '1');
		}

		// Update URL - $effect will detect change via $derived and refetch
		window.history.replaceState({}, '', `?${params.toString()}`);
	};
</script>

<div class="controls">
	<input
		type="text"
		placeholder="Search users..."
		value={search}
		onchange={(e) => updateParams('search', e.target.value || null)}
	/>

	<select value={sortBy} onchange={(e) => updateParams('sort', e.target.value)}>
		<option value="name">Sort by Name</option>
		<option value="email">Sort by Email</option>
		<option value="date">Sort by Date</option>
	</select>
</div>

{#if users.isLoading}
	<p>Loading...</p>
{:else if users.isError}
	<p>Error: {users.isError}</p>
{:else if users.data}
	<table>
		<thead>
			<tr>
				<th style="cursor: pointer" onclick={() => updateParams('sort', 'name')}>Name</th>
				<th style="cursor: pointer" onclick={() => updateParams('sort', 'email')}>Email</th>
			</tr>
		</thead>
		<tbody>
			{#each users.data as user (user.id)}
				<tr>
					<td>{user.name}</td>
					<td>{user.email}</td>
				</tr>
			{/each}
		</tbody>
	</table>

	<div class="pagination">
		<button
			onclick={() => updateParams('page', String(Math.max(1, parseInt(pageNum) - 1)))}
			disabled={parseInt(pageNum) === 1}
		>
			Previous
		</button>
		<span>Page {pageNum}</span>
		<button onclick={() => updateParams('page', String(parseInt(pageNum) + 1))}> Next </button>
	</div>
{/if}

Configuration

Initialize the library globally with Query.setup(options):

Query.setup({
	baseURI: 'https://api.example.com',
	baseInit: {
		headers: {
			Authorization: 'Bearer token'
		}
	},
	cacheTimeout: 2000, // Default cache duration (ms)
	onError: (query, error) => {
		console.error(`Query failed: ${query.endpoint}`, error);
	},
	onSuccess: (query) => {
		console.log(`Query succeeded: ${query.endpoint}`);
	},
	loadingSlowTimeout: 30000, // When to trigger slow loading
	onLoadingSlow: (query) => {
		console.warn(`Slow query: ${query.endpoint}`);
	},
	shouldRetryWhenError: true, // Enable automatic retries
	retryCount: 5, // Number of retry attempts
	retryDelay: 10000 // Delay between retries (ms)
});

Options:

| Option | Type | Default | Description | | ---------------------- | -------- | ------- | -------------------------------------------------------------- | | baseURI | string | - | Base API endpoint | | baseInit | object | - | Default fetch options (headers, credentials, etc.) | | fetcher | function | - | Custom fetch implementation (defaults to native fetch) | | cacheTimeout | number | 2000 | Cache expiration in ms. Use -1 for permanent, 0 to disable | | onError | function | - | Called on error: (query, error) => void | | onSuccess | function | - | Called on success: (query) => void | | loadingSlowTimeout | number | 30000 | Threshold for slow loading indicator (ms) | | onLoadingSlow | function | - | Called when loading exceeds threshold: (query) => void | | shouldRetryWhenError | boolean | false | Automatically retry failed queries | | retryCount | number | 5 | Maximum retry attempts | | retryDelay | number | 10000 | Delay between retries in ms |

Advanced Features

Request Deduplication

When multiple .fetch() calls happen simultaneously on the same endpoint, only one network request is made. Subsequent calls wait for the first request to complete, then return the cached result:

const users = useQuery<User[]>('/users');

users.fetch(); // Network request #1 starts, data gets cached
users.fetch(); // Waits for request #1 to complete (no new request)
users.fetch(); // Waits for request #1 to complete (no new request)

// All await complete when first network request finishes
// Data from request #1 is now cached for all three

Benefits: Prevents duplicate requests when effects/handlers trigger simultaneously. The cached data from the first request satisfies all pending calls.

Automatic Retries

When shouldRetryWhenError: true, failed requests automatically retry based on configuration:

Query.setup({
	shouldRetryWhenError: true,
	retryCount: 5, // Max 5 retries
	retryDelay: 10000 // 10s between attempts
});

// Each error event gets unique ID - if new error occurs before retries complete,
// previous retry sequence is abandoned (prevents thundering herd problem)

Error Object Structure

The isError field contains either a string or Error object with additional properties:

if (query.isError) {
	if (typeof query.isError === 'object' && query.isError.status) {
		console.log(query.isError.status); // HTTP status code
		console.log(query.isError.info); // Parsed response body
	} else {
		console.log(query.isError); // Error message string
	}
}

Cache Behavior

  • Cache Hit: Returns cached data immediately if not expired
  • Cache Expired: Returns old data visually while fetching fresh data in background (may not set isLoading)
  • Stale While Revalidate: Default behavior compatible with real-world UX patterns

Cache TTL modes:

  • -1: Never expires (permanent cache)
  • 0: No caching (always fetch fresh)
  • > 0: Milliseconds until expiration

API Reference

Query Hooks

useQuery<T>(endpoint, options?)

Fetch data from a specific endpoint.

Features:

  • Request deduplication (multiple simultaneous .fetch() = 1 request)
  • Automatic caching with configurable TTL
  • Built-in error handling and retry logic
  • Group management for batch operations
  • Local option overrides (all Query.setup options can be passed)

Parameters:

  • endpoint: API path (baseURI + endpoint)
  • options.cacheTimeout: Override global cache TTL (ms)
  • options.group: Single group tag for query organization (Query.clearGroup() targets this)
  • options.groups: Array of group tags (query appears in multiple groups)
  • options.*: Any Query.setup() option can be overridden locally for this query

Note: Options passed to useQuery apply only to that query instance and override global settings (Query.setup()).

Group Management:

  • A query can have either a single group OR multiple groups tags (or both)
  • Query.group('tag') returns all queries with that tag (from either group or groups)
  • Query.clearGroup('tag') clears all queries associated with that tag
// Single group
const userData = useQuery<User[]>('/users', {
	group: 'user-data'
});

// Multiple groups
const sharedData = useQuery<any>('/shared', {
	groups: ['user-data', 'system-data']
});

// Override retry behavior for this query only
const riskData = useQuery<any>('/risky-endpoint', {
	shouldRetryWhenError: false
});

// Get all queries tagged with 'user-data'
const userQueries = Query.group('user-data'); // includes userData and sharedData

Refetch Methods:

// Initial fetch
await data.fetch();

// Refetch bypassing cache
await data.refetch();

// Refetch suppressing loading state (doesn't hide old data)
await data.refetch({ disableLoading: true });

useDynamicQueries<T>(keyFn, options?) / useSingleQuery<T>(keyFn, options?)

Create multiple queries dynamically based on a key function. Both methods are equivalent.

const posts = useDynamicQueries<Post>((id: number) => `/posts/${id}`);
await posts[1].fetch();
await posts[2].fetch();

mutate(endpoint, options?)

Update cache for a query (doesn't make server requests). Make server mutations separately.

Options:

  • data: Directly set cache data
  • populateCache: Update cache using a function (receives current data)
  • refetch: Force data refresh from server (default: true if neither data nor populateCache provided, false otherwise)
// Update cache directly
await mutate('/users/1', {
	data: { id: 1, name: 'Updated' }
});

// Update cache using function
await mutate('/users', {
	populateCache: (current) => [...current, newUser]
});

// Force refetch even when providing data
await mutate('/users/1', {
	data: { id: 1, name: 'Updated' },
	refetch: true // Will update cache AND fetch fresh data
});

Query Management Methods

Query.clear(endpoint?)

Clears cached query results and resets internal query states.

Query.clear(); // Clear all queries
Query.clear('/users'); // Clear specific endpoint

Query.clearGroup(group)

Clears all queries in a specific group.

Query.clearGroup('user-data');

Query.group(group)

Returns all queries associated with a group.

const userQueries = Query.group('user-data');

Query State & Methods

Each query object provides:

Properties:

query.data; // The fetched data (T | null)
query.isLoading; // Boolean - currently fetching?
query.isError; // Error message string or false
query.endpoint; // The API endpoint string
query.group; // Assigned group tag (if any)
query.groups; // Assigned group tags array (if any)

Methods:

query.fetch()              // Start fetching data
query.refetch(options?)    // Re-fetch with optional config
query.mutate(options?)     // Update cache with new data
query.clear()              // Clear this specific query

Known Limitations & Risks

Memory Growth (Allowed Risk)

The library maintains an unbounded cache and state objects for each unique endpoint. This is by design to maximize performance:

  • Acceptable for: Most applications with <10k unique queries (typical use: static endpoints + pagination)
  • Risk: Heavy dynamic usage may accumulate memory bloat over extended sessions
  • Examples of concern:
    • Fetching 10,000+ unique filtered queries without cleanup
    • Long-running SPA with continuous dynamic parameterization
    • No automatic eviction of old entries

Mitigation strategies:

  • Call Query.clear(endpoint) for stale queries you no longer need
  • Call Query.clearGroup(group) to batch-clear related queries
  • Consider implementing LRU eviction in your application layer
  • Monitor memory in dev tools for long-running sessions

Status: Acknowledged and accepted tradeoff for performance. Not a bug, design choice.

Changelog

See CHANGELOG.md for detailed release notes, bug fixes, and version history.

License

MIT