Skip to content

Runes: Allow effects to be created outside of components #9269

@intrnl

Description

@intrnl

Describe the problem

Within my codebase, I always prefer singletons over passing instances via contexts, this is mostly because these singletons are not something that is expected to change or be different over the entire session, and in the testing side I also believe more in E2E testing more so than individual component testing.

I'll be deferring to Solid.js here as that's something that I've been using often recently, and so I'll be posting snippets of my Solid.js implementations of stuff.

For example, persisting to local storage, I have this implementation for persisting an object to storage. I instantiate this directly on a global module to be imported directly by components/routes that needs them, used to contain app preferences like app theme, currently active accounts, filters, etc. Since the object does have to be persisted, there would have to be a stray effect being created for this.

I also have an implementation for listening to media query changes as a signal that can be read by any effects/components at any time, and by using these two implementations, it would make sense that I would also have a stray effect running outside of components so that my app could choose the appropriate theme to use for the app itself.

Describe the proposed solution

Of course, allowing effects to be run outside of components as is would be a bad idea, it could very much be unintentional, so one way we could do this is to introduce a $root rune that would permit these effects to be ran.

let count = $state(0);

$root(() => {
  $effect(() => {
    console.log('count is', count);
  });
});

count = 2;

$root could return with a function that would allow for the newly created effects to be destroyed

const destroy = $root(() => {
  // ... effect
});

destroy();

or alternatively, we could just pass a destroy function to the callback being called, and $root would return any value from that callback, I think this would be neat if one would like to instantiate everything within said $root, this is what Solid.js also does.

// Let's have a persistent counter
const instance = $root((destroy) => {
  let count = $state(0);

  $effect(() => {
    // persist the count
  });

  return {
    get count () { return count; },
    set count (next) { count = next; },
    destroy,
  };
});

counter.count = 8;

// We don't need it anymore, goodbye!
counter.destroy();

Alternatives considered

The alternative would be to just avoid creating these stray effects, yes, I could just instantiate these helpers, or parts that are necessary for my app, inside components directly, like in the main Svelte component and have it be passed via contexts. But I still don't think these are something that necessitates being passed via contexts.

Another alternative would be to allow effects to be ran outside of components directly, but this is an idea that even I don't agree with, and I think everyone in the dev team also does which is why the error is even there to begin with.

Importance

would make my life easier

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions