-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
Reactive statement cleanup function, like useEffect return value in React #5283
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
There is a library which attempts to shim react hooks, maybe you can just use that The fact this library exists means it's possible to do something similar without having to add new features to the core IMO. |
@dummdidumm Thanks for adding the link. I did take a look at that repo, but it doesn't look like it implements calling cleanup functions. I tried out the code with the REPL and it seems they are not supported. |
I suspect that there is an easier way to do this in Svelte that might not be immediately obvious if you're coming from React. A custom store or an action might work well here. |
@kevmodrome I actually had a similar hook that I was trying to convert to Svelte and using a store creating function in a reactive declaration worked perfectly. React: import { useState, useEffect } from 'react';
import { LocalAudioTrack, LocalVideoTrack, RemoteAudioTrack, RemoteVideoTrack } from 'twilio-video';
type TrackType = LocalAudioTrack | undefined;
export default function useIsTrackEnabled(track: TrackType) {
const [isEnabled, setIsEnabled] = useState(track ? track.isEnabled : false);
useEffect(() => {
setIsEnabled(track ? track.isEnabled : false);
if (track) {
const setEnabled = () => setIsEnabled(true);
const setDisabled = () => setIsEnabled(false);
track.on('enabled', setEnabled);
track.on('disabled', setDisabled);
return () => {
track.off('enabled', setEnabled);
track.off('disabled', setDisabled);
};
}
}, [track]);
return isEnabled;
} Svelte: import type { LocalAudioTrack } from 'twilio-video'
type TrackType = LocalAudioTrack | undefined
export const useIsTrackEnabled = (track: TrackType) => ({
subscribe: (onChange: (v: boolean) => void) => {
onChange(track ? track.isEnabled : false)
if (track) {
const onEnabled = () => onChange(true)
const onDisabled = () => onChange(false)
track.on('enabled', onEnabled)
track.on('disabled', onDisabled)
return () => {
track.off('enabled', onEnabled)
track.off('disabled', onDisabled)
}
}
},
}) Usage in Svelte: export let track
// We get a new store when track is changed, cleanup is handled by store auto subscription when the isTrackEnabled store is used.
$: isTrackEnabled = useIsTrackEnabled(track) For cases where data is not being derived though, and we're using a reactive statement for side effects, this doesn't seem to work well. I would have to render something invisible to get auto subscription / auto unsubscription working. |
Maybe this is just not implemented yet and can be added. |
One thing I considered was abusing a custom store for this kind of thing. I figured I would have to do something like render an invisible element, but just using a reactive statement with // Store is reactive because it should re-evaluate when track is changed.
let store
$: store = {
// We're not trying to get a value out of this, so onChange is never called.
subscribe(onChange) {
const onStop = () => track.stopAll()
track.on('stop', onStop)
return () => {
track.off('stop', onStop)
}
}
}
// This reactive statement is just used to have the store automatically subscribed and unsubscribed.
$: $store Alternatively: const useEffect = (subscribe) => ({ subscribe })
let effect
$: effect = useEffect(() => {
const onStop = () => track.stopAll()
track.on('stop', onStop)
return () => {
track.off('stop', onStop)
}
})
$: $effect |
I've been thinking more about this. I think Svelte has some things to learn from React hooks. Svelte right now has a lot of opportunities to have component state become out of sync with props. The most basic example I can think of is a label like this: <script>
import { onMount } from 'svelte'
export let text
let element
onMount(() => {
console.log(text)
element.innerText = text
})
</script>
<h1>
{text}
</h1>
<h1 bind:this={element} > </h1> When the Solving this sort of issue is an advantage of React hooks. This is discussed in React as a UI Runtime, you can search for "For example, this code is buggy:". It's explaining that if the dependency array is not correct and the effect is not re-run then it becomes out of sync. If Svelte came up with some kind of hooks like API maybe it could solve both these issues at once. |
Just use I'm not sure I understand the problem, everything you are describing is already possible. If you want to perform some cleanup everytime a reactive declaration runs then add that cleanup before your side effect. If you want to cleanup when a component dismounts, use While react hooks were one of the catalysts for v3 we don't agree with with the APIs or the model and won't be emulating it. |
Cleanup is also required when dependencies (props in the example) change. As things are I think there will be many cases where components do not reflect their props in Svelte code if people are just using the lifecycle methods for these kinds of things.
I'm suggesting this is a problem generally. Users will not think of being out of sync with props when writing I'm not suggesting that Svelte should re-render like React does, or have dependency arrays, I'm suggesting there should be a way to write lifecycle related code that also responds to changing props, like how I think Svelte's automatic/compiled reactivity is great. I think it just needs a few changes, possibly non-breaking additions, to be as powerful as hooks, when it comes to abstracting lifecycle related logic, and making it easy to keep effects in sync with props. This actually does almost exactly what I want, and could almost be used to replace let effect
$: effect = useEffect(() => {
const onStop = () => track.stopAll()
track.on('stop', onStop)
return () => {
track.off('stop', onStop)
}
})
$: $effect If there was a version of that that waited for actual mounting, behaved exactly the same, but was slightly less verbose, that would be perfect, while working the same way as the rest of Svelte (no needless re-rendering, no dependency arrays). React version of the last REPL, for comparison. |
I agree with @DylanVann. Svelte should implement an official way to take advantage of hooks. |
I came across this (via https://dylanvann.com/svelte-version-of-useeffect) while looking for a way to do cleanup (unsubscribe and resubscribe) in reaction to a changes to props in Svelte.
@dummdidumm, @DylanVann: Indeed, it looks like svelte-hooks did add support for clean-up functions to their @DylanVann, I updated your Svelte Hooks Testing Cleanup REPL to use that latest version and the clean-up seems to work now (let me know if not; I don't completely understand your example). I also created a more minimal example REPL based on your React sandbox. I guess the workarounds kinda work, but I would rather see first-class support for this. The workarounds so far are too verbose and ugly, and (like @DylanVann says) don't feel like idiomatic Svelte. Things that users of a framework should be doing (like reacting to changes in props) should be encouraged by the framework by providing first-class support for them and making them super easy. (BTW, being able to subscribe to an expression like one of the ideas in #4079 could make this a little more concise (eliminate 2 lines of boilerplate): Personally, I think the solution @DylanVann suggested (allowing reactive statements to optionally providing a cleanup function) seems pretty reasonable: $: () => {
const onStop = () => track.stopAll()
track.on('stop', onStop)
return () => {
track.off('stop', onStop)
}
} It would be pretty backwards compatible since most/all existing reactive statements don't currently look (to the parser) like a function, since a function by itself (without invoking it or assigning it to a variable) would have no effect (no pun intended). |
While I don't agree with everything OP said here, I do agree that this is something that's generally valuable. Sure, you can simulate this yourself with a bunch of boilerplate, but it's pretty annoying. Here's the basic way of doing this: $: _cleanup = (() => {
if (_cleanup) {
_cleanup();
}
// Do something
return () => { /* Do cleanup */ };
})();
onDestroy(() => {
if (_cleanup) {
_cleanup();
}
}); That's a lot of boilerplate. Ideally you could reduce this to something like: $: runWithCleanup(() => {
// Do something
return () => { /* Do cleanup */ };
}); but if you actually try implementing this you will run into an issue keeping track of which call corresponds to which cleanup function when runWithCleanup is used multiple times within one component. React hooks solve this problem by requiring hooks to always be called in the same order, so they can just use the execution order. But obviously that doesn't work with Svelte since Svelte's reactivity model is more fine-grained & only selectively updates $ statements on invalidations. If you want to keep this same API, the best I can see is using (You also need to do something similar to As far as I can see, you need a framework change to actually get really good ergonomics for this use case. If this were done at the compiler level it could be done without much cost to users. To me the only question would be whether it's felt that this is useful enough to add in. Personally I feel like there's a reason React supports this--it's pretty common for something with side effects to need to be cleaned up on changes. Lots of subscription type APIs, like IntersectionObserver, or perhaps something that mutates some other part of the DOM (not the component's DOM where an action would make more sense). Am I missing anything here? Edit: thinking about this more, I think an API using a string identifier is really not the end of the world. Though it would be nice if there were a cleaner version built-in, I think I'm going to just go with this and maybe publish it somewhere for other people to use too. |
I agree with @DylanVann. |
Any reason this is closed? Searched around wanting the same thing. |
For anyone stumbling across this issue, I found a solution for cleanup on dependency change on this great blog post: $: (_cleanup = (()=>{
if (_cleanup){
_cleanup();
}
doSomething(a,b);
return cleanup;
})()); Or with component lifecycle:$:(_cleanup=tick()
.then(_cleanup||Promise.resolve)
.then(()=>{
doSomething(a,b);
return cleanup;
})
); But I agree, the syntax is pretty hard to read. |
Another option is to use This is probably not ideal performance wise, but for values that rarely change (like the current user) it can be a lot simpler than making sure everything is cleaned up on every render. |
+1, I would like something like this. I have created a slider that is either a row of images or a slider depending on the width of the browser. Conditionally changing whether it should be a slider or not is difficult because technically it doesn't unmount when the browser width changes, but I also add a lot of code and event listeners when the browser becomes a certain width. At the moment onDestory will not work so I'm not entirely sure how to properly clean this when it stops becoming a slider. On thing I tried was a hidden div that was wrapped in a I would love to see a solution to this. |
Svelte v5 announced Runes. Take this Firebase Firestore example: <script>
import { doc, onSnapshot } from "firebase/firestore";
let city = $state("SF")
$effect(() => {
// runs when the component is mounted, and again
// whenever `city` change,
// after the DOM has been updated
const unsub = onSnapshot(doc(db, "cities", city), (doc) => {
console.log("Current data: ", doc.data());
});
return () => {
// if a callback is provided, it will run
// a) immediately before the effect re-runs
// b) when the component is destroyed
unsub()
}
})
</script> You can even export it from an external module (like hooks): import { doc, onSnapshot } from "firebase/firestore";
export function createFirestoreListener() {
let city = $state("SF")
let cities = $state([])
$effect(() => {
const unsub = onSnapshot(doc(db, "cities", city), (doc) => {
cities = doc.data()
});
return () => unsub
})
function setCity(value) {
city = value
}
return {
// We need to use a getter here or the value will always be
// whatever it is at the time function was called.
get cities() { return cities },
get city() { return city },
setCity
}
} <script>
import { createFirestoreListener } from './createFirestoreListener'
const data = createFirestoreListener()
</script>
<CityList cities={data.cities} />
<input value={data.city} on:input={(e) => data.setCity(e.target.value)} /> |
Hi! While Svelte 5 has fixed this, I've released svelte-cleanable-store, a custom Svelte-compatible store that supports cleanup, like Usage example: import { cleanable } from "svelte-cleanable-store";
const state = cleanable(0);
state.subscribe((value) => {
console.log(`state = ${value}`);
return () => console.log(`cleaning up ${value}...`);
}); This is a very clean workaround for Svelte 4 at the moment, and since it's a store, it might even still be useful when Svelte 5 is out, as it can be used in any |
Is your feature request related to a problem? Please describe.
I'm trying to port some code that uses React hooks to Svelte. React has
useEffect
, which can be used to run some code when its dependencies change. I think the Svelte equivalent would be reactive statements ($: { doStuff() }
).When using
useEffect
you can return a function, and that function will be called when dependencies change, or when the component using theuseEffect
is unmounted. I do not see an equivalent in Svelte.Here are the relevant docs for
useEffect
: https://reactjs.org/docs/hooks-reference.html#cleaning-up-an-effectDescribe the solution you'd like
I think it might be beneficial for Svelte to allow for returning cleanup functions in reactive statements.
In order to allow for returning though I think it would need to be possible to give a function as a reactive statement. Not sure, I'm hoping something like this could be possible though.
For reference in React this would look like:
Describe alternatives you've considered
The closest I could come up with is this:
This is verbose compared to how
useEffect
works. As a React user this seems like a step backwards, feeling more likeclass
components, lifecycle methods, instance variables, instead of clean like the hooks version.This could be cleaned up a bit by initializing
cleanup
:How important is this feature to you?
It's important to me because I'm trying to convert React code to Svelte code and there doesn't seem to be a clean translation of this common React feature (
useEffect
+ cleanup function).I believe many other users may come from the React ecosystem and encounter this issue.
The text was updated successfully, but these errors were encountered: