Nov 02, 2025
4 min read

When $effect() Destroys Your User's Input: Svelte 5 Reactivity Gotchas

Svelte 5's $effect() can silently erase user input if you're not careful about when you trigger updates.

I spent an hour debugging why my todo form kept eating user input. They’d type “@phone” and the cursor would jump. They’d type “+Fam” and poof—gone. Classic Svelte 5 reactivity trap.

Turns out, $effect() is both brilliant and dangerous. In Svelte 5, it’s the new way to run side effects when reactive values change. But if you’re not careful about what you’re watching, it’ll fire on every keystroke and destroy what your user is typing.

The Problem: Parsing on Input

Here’s what I was doing wrong. I had a form where users could type todo.txt format directly—something like “(A) Call Mom @phone +Family due:2024-10-27”. When they changed structured fields (priority, due date, etc.), I wanted to update the raw text. Seemed simple enough!!

$effect(() => {
  if (isInitialized) {
    // Watch these fields and update rawText when they change
    priority;
    dueDate;
    thresholdDate;
    recurrence;
    // Trigger update
    updateRawText();
  }
});

But I also had this handler:

function handleRawTextInput(e) {
  const text = e.target.value;
  rawText = text;

  // Parse the text to extract priority, dates, etc.
  parseRawText();  // THIS IS THE PROBLEM!!
}

Every keystroke triggered parseRawText(), which updated priority, dueDate, etc. Those updates triggered the $effect(), which called updateRawText(), which reformatted the entire line. The user would type “@ph” and suddenly it would reformat to whatever the parser thought it should be. Cursor position lost. User frustrated.

The Fix: Parse Only on Blur

The key insight was this: don’t parse while the user is actively typing. Only parse when they’re done.

function handleRawTextInput(e) {
  const text = e.target.value;
  rawText = text;
  cursorPosition = e.target.selectionStart;

  // Check for autocomplete triggers (@ or +)
  // ... autocomplete logic ...

  // DON'T parse here! Parsing on every keystroke triggers the $effect()
  // which calls updateRawText() and destroys user input.
  // We parse on blur instead.
}

And in the template:

<textarea
  bind:value={rawText}
  oninput={handleRawTextInput}
  onblur={parseRawText}
  placeholder="(A) Call Mom @phone +Family due:2024-10-27"
/>

Now the flow is clean. User types freely. When they tab out or click away, we parse and sync to structured fields. When they change a structured field (via the priority dropdown or date picker), the $effect() updates the raw text. No more cursor jumping. No more lost input.

The Initialization Guard

One more gotcha: $effect() runs immediately on mount. If you’re not careful, it’ll overwrite your initial values before the user even sees them.

let isInitialized = $state(false);

// After initial load, watch for changes to form fields and update rawText
$effect(() => {
  if (isInitialized) {
    priority;
    dueDate;
    // ... watch these values
    updateRawText();
  }
});

// Mark as initialized after first render
$effect(() => {
  isInitialized = true;
});

The trick is to have a separate $effect() that sets isInitialized = true. This runs once on mount, and from then on, the first effect can safely update things.

Lessons Learned

Svelte 5’s reactivity is incredibly powerful, but you need to be deliberate about when side effects fire. If you’re working with user input:

Don’t parse or transform input on every keystroke. Use onblur instead. Don’t let $effect() run before your component is fully initialized. Use a flag. Think about the cycle: does your input handler trigger reactive values that trigger effects that update the input? If so, you’ve got a loop.

The most frustrating bugs are the ones where the code works perfectly from the computer’s perspective. It’s just doing exactly what you told it to do—updating values, triggering effects, reformatting text. But from the user’s perspective, their input is being eaten. Always test by actually typing, not just by programmatically setting values.

References