Skip to content
ExponaBLOG
SVELTE & SVELTEKIT · DAY 071 / 100

Svelte interview questions: patterns, pitfalls and fixes

Questions for Svelte developers.

Expona Admin7 min read

Svelte interview questions often reveal whether a candidate understands the framework's core philosophy: compiling away the framework overhead and embracing fine-grained reactivity. In this guide, we walk through key concepts that separate experienced Svelte developers from those still thinking in React terms, covering patterns that work, pitfalls that catch teams off guard, and the fixes that keep projects running smoothly.

How does Svelte differ from React?

The most common Svelte interview question starts here, and the answer goes beyond "Svelte is smaller." The fundamental difference is compilation strategy. React ships the framework runtime to the browser; Svelte compiles your components into vanilla JavaScript at build time. This means no virtual DOM diffing, no reconciliation overhead, and smaller bundle sizes by default.

In React, reactivity is implicit. You declare state with useState, and React re-renders the component when state changes. You must then optimize with useMemo, useCallback, and other hooks to prevent unnecessary renders. In Svelte, reactivity is fine-grained. The compiler tracks variable assignments and automatically generates update code only for the exact DOM nodes that depend on that variable.

Consider a counter component. In React:

typescript
export default function Counter() {
  const [count, setCount] = useState(0);
  return (
    <>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </>
  );
}

React will re-run the entire component function and create a new virtual tree when count changes. In Svelte:

typescript
<script>
  let count = 0;
</script>

<p>Count: {count}</p>
<button on:click={() => count++}>+</button>

The Svelte compiler transforms this into direct DOM updates. The button click increments count, and the compiler-generated code updates only the <p> text node. No virtual DOM, no reconciliation. This matters at scale: applications with thousands of reactive bindings feel snappier on low-end devices because Svelte generates less JavaScript.

What are runes?

Runes are a more recent addition to Svelte (introduced in Svelte 5) that formalize the compiler signals developers were already using informally. Think of runes as explicit hints to the compiler about intent and reactivity scope. The main runes are $state, $derived, and $effect.

$state declares a reactive variable. Before runes, you'd write let count = 0 in a component script, and it was implicitly reactive. With runes, you make it explicit:

typescript
<script>
  let count = $state(0);
  let name = $state('Alice');
</script>

This becomes powerful in reusable logic. If you extract this into a function called a "rune" or "composable," the compiler knows which variables drive reactivity:

typescript
function useCounter(initial = 0) {
  let count = $state(initial);
  return {
    count: () => count,
    increment: () => count++,
    decrement: () => count--,
  };
}

$derived creates a reactive dependency. When the source changes, the derived value recomputes:

typescript
<script>
  let count = $state(0);
  let doubled = $derived(count * 2);
</script>

<p>Count: {count}, Doubled: {doubled}</p>

The compiler sees that doubled depends on count and regenerates the computation whenever count changes.

$effect is Svelte's answer to side effects. It runs after the DOM updates:

typescript
<script>
  let count = $state(0);
  
  $effect(() => {
    console.log('Count changed to', count);
    document.title = `Count: ${count}`;
  });
</script>

Unlike React's useEffect, $effect automatically tracks dependencies. The compiler sees you reference count and re-runs the effect when count changes. No dependency array needed.

Universal vs server load?

This question trips up candidates because SvelteKit offers two places to load data: universal load and +page.server.js load. The distinction matters for security, performance, and where your code runs.

Universal load runs on the server during SSR and on the client during client-side navigation. Put it in +page.js:

typescript
// src/routes/products/+page.js
export async function load({ fetch, params }) {
  const res = await fetch(`/api/products/${params.id}`);
  const product = await res.json();
  return { product };
}

When a user navigates to this page server-side (page load), SvelteKit runs this function on the server and passes product to the component. When the user navigates client-side (from another page), SvelteKit runs this function on the client. The fetch parameter is a special fetch that works in both contexts.

Server load runs only on the server. Use it in +page.server.js:

typescript
// src/routes/products/+page.server.js
import { db } from '$lib/database';

export async function load({ params, cookies }) {
  const userId = cookies.get('user_id');
  const product = await db.products.findById(params.id);
  const userPreferences = await db.users.getPreferences(userId);
  return { product, userPreferences };
}

This function never runs on the client. It has access to secrets, database connections, and cookies—things you don't want exposed to the browser. On client-side navigation, SvelteKit calls this server function via an RPC-like mechanism and returns the result.

Pitfall: Putting an API key in a universal load leaks it to the browser. Move that logic to a server load instead.

Performance note: Universal load is cheaper for client-side navigation because it doesn't require a network round-trip. Server load must be fetched from the server every time. Choose based on whether you need server-only data.

How do form actions work?

Form actions let you handle POST, PUT, DELETE, and PATCH requests without writing an API route. Define actions in +page.server.js:

typescript
// src/routes/todos/+page.server.js
export const actions = {
  create: async ({ request }) => {
    const data = await request.formData();
    const title = data.get('title');
    
    const todo = await db.todos.create({ title });
    return { success: true, todo };
  },
  
  delete: async ({ request }) => {
    const data = await request.formData();
    const id = data.get('id');
    
    await db.todos.delete(id);
    return { success: true };
  },
};

export async function load() {
  const todos = await db.todos.list();
  return { todos };
}

In the component, use the <form> element with the action attribute:

typescript
<script>
  import { enhance } from '$app/forms';
  export let data;
</script>

<ul>
  {#each data.todos as todo (todo.id)}
    <li>
      {todo.title}
      <form method="POST" action="?/delete" use:enhance>
        <input type="hidden" name="id" value={todo.id} />
        <button type="submit">Delete</button>
      </form>
    </li>
  {/each}
</ul>

<form method="POST" action="?/create" use:enhance>
  <input name="title" placeholder="New todo" />
  <button type="submit">Add</button>
</form>

The use:enhance directive progressively enhances the form. On modern browsers with JavaScript, it intercepts the submission, sends it as a fetch request, and updates the page without a full reload. On older browsers or if JavaScript fails, the form submits normally as HTML.

The action name comes from the query parameter: ?/create calls the create action. The function receives the form data, processes it, and returns a result. SvelteKit automatically re-runs the load function after the action completes, keeping the page in sync.

Common mistakes

  • Forgetting that universal load runs on the client. Candidates often assume server-side logic is hidden and put secrets or credentials there. Audit your +page.js files and move sensitive operations to +page.server.js.
  • Not using $derived and reaching for $effect instead. A common pattern is running an effect to compute a value whenever another changes. Use $derived instead. It's clearer, more efficient, and the compiler optimizes it better.
  • Wrapping every variable in $state. In components, plain let declarations are reactive by default. You only need $state when crossing module boundaries or in advanced patterns. Overusing it adds noise.
  • Ignoring form validation on the server. Actions are invoked from the client, so validate input. Don't trust request.formData() blindly. Validate types, lengths, and business rules.
  • Returning large nested objects from load. If your load function returns a deeply nested object, serialization can be slow. Flatten the structure when possible.

Key takeaways

  • Svelte compiles components and uses fine-grained reactivity, eliminating the virtual DOM overhead React requires.
  • Runes ($state, $derived, $effect) formalize compiler signals and make reactivity explicit and portable.
  • Universal load runs client and server; server load runs server-only. Choose based on data sensitivity and navigation patterns.
  • Form actions are server-side handlers for native form posts with optional progressive enhancement via use:enhance.
  • Always validate form input server-side and keep secrets in server load functions, never universal load.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles