Svelte Components Are Not Resilient
How common Svelte patterns encourage non-resilient components.
The term "resilient components" here is coming from Dan Abramov's article Writing Resilient Components.
The principles he outlines are:
- Don’t stop the data flow.
- Props and state can change, and components should handle those changes whenever they happen.
- Always be ready to render.
- A component shouldn’t break because it’s rendered more or less often.
- No component is a singleton.
- Even if a component is rendered just once, your design will improve if rendering twice doesn’t break it.
- Keep the local state isolated.
- Think about which state is local to a particular UI representation — and don’t hoist that state higher than necessary.
The principle we're discussing in relation to Svelte is "Don't stop the data flow.", otherwise phrased as "Props and state can change, and components should handle those changes whenever they happen.".
Let's start by writing a Svelte component.
We want to write a component that fetches some photos from an API (this is taken from the onMount
example in the Svelte docs).
PhotoGrid [Svelte]:
Now that we have a nice PhotoGrid
component, let's write a component for selecting an album:
PhotoGrid With AlbumSelector [Svelte] (broken):
Beautiful, except that switching albums does not update the PhotoGrid
.
This is not the automatic reactivity we were promised by Svelte.
Why does it not work?
The answer is that onMount
is not reactive, and so our component is not reactive. onMount
does not rerun when our props change, so it does not fetch new data when the album prop is changed.
If we were to write our PhotoGrid
component in React (with hooks) it would look something like this:
PhotoGrid with AlbumSelector [React] (not broken):
This component is almost the same as our Svelte version, and yet it works when we change albums, the photos for the selected album are fetched!
The key difference is that we use useEffect
instead of onMount
.
Using the dependency array we tell React that the effect (fetching photos) should rerun when the album
prop is changed.
Even if we didn't think of this dependency ourselves eslint-plugin-react-hooks
would tell us about it.
When using React hooks there is no concept of onMount
because the idea of only running some code on mount leads to writing non-resilient components,
components that do one thing when they mount, and then don't take prop changes into account.
Previously with React's class component API we would need to use componentDidUpdate
, React hooks improves upon this, it encourages writing resilient components from the start, and we can also write cleanup logic in the same useEffect
call.
To fix our Svelte version you might think we could use beforeUpdate
or afterUpdate
,
but these lifecycle functions are related to the DOM being updated, not to prop updates.
We only want to rerun our fetching when the album prop is changed.
What is the best way to fix this then? I'm not sure what the best way is, but one way would be to implement useEffect
for Svelte ourselves:
PhotoGrid with AlbumSelector [Svelte] (fixed with useEffect):
With this code we use Svelte's reactivity and a custom store to have our fetch rerun whenever the album prop is changed.
It works, I'm not sure this will work with SSR though, and the code is a bit verbose/convoluted.
IMO Svelte should make something like useEffect
part of the framework so that this could work better and be less verbose.
Conclusion
- Our custom
useEffect
is not idiomatic Svelte. - Idiomatic Svelte uses
onMount
which results in non-resilient components. - Idiomatic React (Hooks) uses
useEffect
which results in resilient components, assuming you listen to the linter.
I do like the direction Svelte is heading but I think this is one area that could be improved.