Skip to content

More detailed explanation of why using $effect is not a good idea if it can be avoidedΒ #14697

@leonidaz

Description

@leonidaz

Describe the bug

I think the explanation of when not to use $effect's is rather lacking in terms of the exact reasons why. Instead of just saying don't do this..., which is fine, it would more helpful for learning to understand the exact reasons.

This section: https://svelte.dev/docs/svelte/$effect#When-not-to-use-$effect

Starting a summary of the reasons:

  1. Using effects is generally more overhead due to how it works internally, and signals in general, versus using something like simple callbacks. For example:
    • Effects have to run at least once regardless whether or not they read any states / signals.
    • if you return a function (dispose function) from an $effect, it runs not only when a component is destroyed but every time before the $effect is executed again.
    • Even if you don't return a dispose function, internally, before the $effect is run again, its own dispose actions have to run, like removing all previous subscriptions.
    • Effects run every time any of the observable states / signals change, and every time subscriptions to these changes have to be set up anew.
  2. Mutating states / signals inside effects can cause unintended reads of these states / signals, thus causing unintended subscriptions. In turn, these will lead to infinite loops. Using untrack() or async code should remedy such situations, however, it may not be sufficient (see below).
  3. If mutating the same states / signals that are observed by the same $effect, even if mutations happen inside untrack() or async code, it will lead to an infinite loop. It can be remedied by using conditional mutations.
  4. Mutating state inside effects across multiple effects can lead to circular logic. For example, given two states a and b with 2 separate effects for each, if the effect for a mutates b, it will cause b's effect to run and if b's effect mutates a, it will cause the a's effect to run, thus causing an infinite loop. Conditional mutations can resolve such infinite loops due to circular logic, however, they may be difficult to discover across multiple effects and components.
  5. Understanding the timing of when effects run is important to avoid unexpected behavior.
    • $effect() runs only after components mount or after the template (which is also a type of effect) is updated, whereas $effect.pre() is executed before.
    • Effects are completely skipped and not executed in SSR mode. Even changing any of the state / signals variables will not cause the effects to run.

In summary, there are 3 main reasons: usage overhead, infinite loops because of mutations and timing. I broke out each infinite loop case separately for easier reference to each but they can just be under one section.

Anything else?

Also, the phrase at the end of this section is not accurate because of the reason 3. above:

If you absolutely have to update $state within an effect and run into an infinite loop because you read and write to the same $state, use [untrack](https://svelte.dev/docs/svelte/svelte#untrack).

Reproduction

docs

Logs

No response

System Info

docs

Severity

annoyance

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions