Data invalidation: a practical Svelte & SvelteKit guide
When a user submits a form, deletes an item, or changes a setting, your data becomes stale. SvelteKit provides several mechanisms to keep your UI in sync with server state without forcing a full page reload. Understanding data invalidation is essential for building responsive applications that feel snappy while maintaining data consistency across your pages.
Why data invalidation matters
In traditional server-rendered applications, form submissions naturally reload the page and fetch fresh data. But with SvelteKit's progressive enhancement and client-side navigation, you need to explicitly tell the framework when to rerun your load functions. Without proper invalidation, users see stale data, leading to confusion and bugs that are hard to track down.
Consider an e-commerce cart. A user adds an item via a form action. The cart badge should update, the recommendations should refresh based on new inventory, and the checkout page should reflect the current total. Without invalidation, some of these updates might not happen automatically.
invalidate — Rerun loads that depend on a URL
The invalidate function reruns any load function that depends on a specific URL. This is your surgical tool for targeted data updates.
// src/routes/products/+page.svelte
<script>
import { invalidate } from '$app/navigation';
async function handleAddToCart(productId) {
const response = await fetch('/api/cart', {
method: 'POST',
body: JSON.stringify({ productId })
});
if (response.ok) {
// Only rerun loads that depend on /api/cart
invalidate('/api/cart');
}
}
</script>
<button on:click={() => handleAddToCart(123)}>
Add to Cart
</button>In your load function, declare the dependency:
// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ fetch }) => {
const cart = await fetch('/api/cart').then(r => r.json());
return { cart };
};When you call invalidate('/api/cart'), SvelteKit knows to rerun this load function and pass the fresh cart data to your component. This pattern scales well: multiple components can depend on the same URL, and they all get fresh data without redundant API calls.
invalidateAll — Rerun every load on the page
Sometimes a change affects multiple data sources. invalidateAll reruns every load function, regardless of URL dependencies. Use this when you're unsure of all the side effects or when a single action has wide-ranging consequences.
// src/routes/admin/settings/+page.svelte
<script>
import { invalidateAll } from '$app/navigation';
async function saveSettings(formData) {
const response = await fetch('/api/admin/settings', {
method: 'PUT',
body: JSON.stringify(formData)
});
if (response.ok) {
// Rerun all loads on the current page
invalidateAll();
}
}
</script>Be cautious with invalidateAll in high-traffic scenarios. If your page has five load functions making external API calls, invalidating all of them creates five concurrent requests. It's often better to be explicit with invalidate when possible.
A practical scenario: after a user changes their authentication level, you want to refresh their permissions, their accessible resources, and their audit log. Instead of tracking which URLs are affected, invalidateAll ensures nothing slips through.
depends — Declare custom dependencies
URL-based invalidation works well for API endpoints, but sometimes your load logic depends on non-URL state. The depends function lets you declare custom dependency keys that you can later invalidate.
// src/routes/dashboard/+page.server.ts
import { depends } from '$app/navigation';
export const load = async ({ url }) => {
// Declare that this load depends on a custom key
depends('data:user-preferences');
const preferences = await getUserPreferences();
return { preferences };
};Then invalidate that custom key from your component or action:
// src/routes/dashboard/+page.svelte
<script>
import { invalidate } from '$app/navigation';
async function updateTheme(newTheme) {
await fetch('/api/preferences', {
method: 'POST',
body: JSON.stringify({ theme: newTheme })
});
// Rerun any load that depends on 'data:user-preferences'
invalidate('data:user-preferences');
}
</script>Custom dependencies shine when your load logic doesn't map cleanly to URLs. Perhaps you're loading data from a cache layer, a WebSocket subscription, or a complex business calculation. By using a semantic key like data:user-preferences or cache:recommendations, your code becomes self-documenting and easier to refactor.
Form actions — Successful actions invalidate automatically
SvelteKit's form actions provide automatic invalidation for free. When a +page.server.ts form action succeeds, SvelteKit automatically reruns all load functions on that page. This is the recommended flow for mutations.
// src/routes/todos/+page.server.ts
import type { PageServerLoad, Actions } from './$types';
export const load: PageServerLoad = async ({ fetch }) => {
const todos = await fetch('/api/todos').then(r => r.json());
return { todos };
};
export const actions = {
create: async ({ request }) => {
const formData = await request.formData();
const title = formData.get('title');
const response = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ title })
});
if (!response.ok) {
return { error: 'Failed to create todo' };
}
// After this action succeeds, SvelteKit automatically reruns load()
return { success: true };
}
} satisfies Actions;In your component, use the <form> element with method POST:
// src/routes/todos/+page.svelte
<script>
import type { PageData } from './$types';
export let data: PageData;
</script>
<form method="POST" action="?/create">
<input type="text" name="title" required />
<button type="submit">Add Todo</button>
</form>
<ul>
{#each data.todos as todo (todo.id)}
<li>{todo.title}</li>
{/each}
</ul>This approach is powerful because it combines form handling, validation, and data refresh in one declarative pattern. The form automatically serializes, the action validates and persists, and the UI updates without you writing invalidation code. For failed actions, loads don't rerun, so users can correct their input and try again.
Combining approaches in real-world scenarios
A typical SaaS application uses all three mechanisms together. A form action handles critical mutations. Custom dependencies track cache layers. Targeted invalidate calls handle side effects from third-party integrations.
// src/routes/accounts/[id]/members/+page.server.ts
import { depends } from '$app/navigation';
export const load = async ({ params, fetch }) => {
// URL-based dependency
const account = await fetch(`/api/accounts/${params.id}`).then(r => r.json());
// Custom dependency for the member cache
depends('cache:team-members');
const members = await fetch(`/api/accounts/${params.id}/members`).then(r => r.json());
return { account, members };
};
export const actions = {
addMember: async ({ params, request }) => {
const formData = await request.formData();
const response = await fetch(`/api/accounts/${params.id}/members`, {
method: 'POST',
body: formData
});
if (response.ok) {
return { success: true };
// Automatic invalidation of all loads happens here
}
}
} satisfies Actions;Common mistakes
- Calling invalidate without the right dependency: If your load function doesn't depend on the URL you're invalidating, nothing happens. Double-check that your load function actually fetches from that URL or calls
depends()with that key. - Invalidating from within a load function: Load functions run during navigation. Calling
invalidateinside a load can create infinite loops or race conditions. Invalidate from components or actions instead. - Overusing invalidateAll for performance-critical pages: If your dashboard page has ten independent load functions pulling data from different services,
invalidateAllmight cause a thundering herd. Use targeted invalidation to avoid unnecessary API calls. - Forgetting that form actions auto-invalidate: Many developers manually call
invalidateafter a form action succeeds, duplicating work. Trust the framework's built-in behavior unless you have a specific reason to override it. - Not handling partial failures: If one load function fails during invalidation, others might still run. Your components should handle the case where some data is fresh and some is stale.
Key takeaways
- Use
invalidate(url)for surgical, targeted data refresh when you know exactly which loads are affected. - Use
depends(key)to create semantic dependencies that don't map to URLs, keeping your code maintainable as logic evolves. - Rely on automatic invalidation from form actions for mutations; it's the recommended path and requires no extra code.
- Reserve
invalidateAll()for rare cases where a single change affects many data sources, or when refactoring and you're unsure of all dependencies. - Test invalidation behavior by adding network throttling in DevTools; ensure your app remains responsive and doesn't create unnecessary API calls.
- Combine URL-based and custom dependencies in larger applications to balance clarity with flexibility.






Comments
Loading comments…