Svelte logo

SvelteKit by Example: Refreshing Data

Writing to a database doesn't automatically update data already displayed on the page. Invalidate a load dependency to fetch a fresh copy.

In this example, we'll delete a todo through our API and refresh the list.

Use the endpoints and database contract from the API endpoints example. These dynamic routes need a server runtime. Don't prerender this page; if a parent layout enables prerendering, the export below overrides it.

Use the fetch passed to a universal load. SvelteKit automatically tracks /api/todos as a dependency.

Check the status and content type before reading JSON. Error responses aren't always JSON.

src/routes/todos/+page.js
import { error } from '@sveltejs/kit';

export async function load({ fetch }) {
  const response = await fetch('/api/todos');






  if (!response.ok) error(502, 'Could not load todos');
  const type = response.headers.get('content-type') ?? '';
  if (!type.includes('application/json')) {
    error(502, 'Expected JSON from the todo API');
  }

  return { todos: await response.json() };
}

export const prerender = false;

Keep a busy flag and a message for failures. Fetch rejects on network errors, but not on HTTP error statuses.

Only invalidate after a successful write. The 204 response has no JSON body.

Awaiting invalidation keeps the controls disabled until the new data arrives. The updated data prop redraws the list.

Disable deletion while a request is in progress.

src/routes/todos/+page.svelte
<script>
  import { invalidate } from '$app/navigation';
  let { data } = $props();
  let busy = $state(false);
  let message = $state('');


  async function remove(id) {
    if (busy) return;
    busy = true;
    message = '';
    try {
      const response = await fetch(`/api/todos/${encodeURIComponent(id)}`, {
        method: 'DELETE',
      });


      if (!response.ok) {
        throw new Error('Could not delete the todo.');
      }






      await invalidate('/api/todos');
    } catch {
      message = 'Could not delete or refresh. Reload to check the list.';
    } finally {
      busy = false;
    }
  }
</script>

{#each data.todos as todo (todo.id)}
  <p>{todo.task}: {todo.completed ? 'Done' : 'To do'}</p>
  <button disabled={busy} onclick={() => remove(todo.id)}>Delete</button>
{/each}
<p role="status">{message}</p>

Custom Dependencies

As an alternative to the universal load, read the database in a server load. Database helpers aren't tracked automatically, so declare a custom dependency.

Return the same { todos } shape. In the component above, replace invalidate('/api/todos') with invalidate('app:todos').

src/routes/todos/+page.server.js
import { getTodos } from '#lib/server/db.js';

export async function load({ depends }) {
  depends('app:todos');







  return { todos: await getTodos() };
}

Use one load approach at a time. For the server alternative, replace +page.js with only export const prerender = false;. Custom keys start with lowercase letters and a colon. A URL dependency must match the fetched URL, including its search parameters.

For a manual refresh of everything on the active page, use refreshAll. In SvelteKit 3 it's preferred over invalidateAll: it refreshes loads and active remote functions without clearing page.state.

src/lib/RefreshTodos.svelte
<script>
  import { refreshAll } from '$app/navigation';
  let busy = $state(false);
  let message = $state('');

  async function refresh() {
    if (busy) return;
    busy = true;
    message = '';
    try {
      await refreshAll();
    } catch {
      message = 'Could not refresh todos.';
    } finally {
      busy = false;
    }
  }
</script>

<button disabled={busy} onclick={refresh}>Refresh todos</button>
<p role="status">{message}</p>