The term “resilient components” here is coming from Dan Abramov’s article Writing Resilient Components.
The principles he outlines are:
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).
Now that we have a nice PhotoGrid
component, let’s write a component for selecting an album:
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:
This component is written to be 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:
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.
useEffect
is not idiomatic Svelte.onMount
which results in non-resilient components.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.
Disclaimer: I’m new to Svelte so this isn’t so much a recommendation as it is a “I guess this is a way to do it 🤷♂️”.
October 25, 2020: I am less new to Svelte now. I’ve built a few sites and apps, as well as some open source tools:
Even at the time of writing I had read all of the Svelte docs and gone through all the tutorials, so I was not entirely clueless.
I do still consider this a pain point when writing Svelte code. I’m hopeful that it will eventually be improved upon.
I’ve been playing around with Svelte lately, trying to port some React code to it as an experiment.
A challenge I came across is that I could not find a Svelte equivalent of useEffect
.
Svelte has support for turning a listener based API into a custom store that can also be reactive.
This replaces some usages of useEffect
.
However, for cases where we want to perform reactive effects that only trigger side effects I was unsure how to port the code to Svelte.
The React code to port:
You can see that when the track instance is swapped we remove the listener on the previous track, and add one on the new track.
The behavior we want is:
stop
event on track
.stopAllTracks()
method on the track (don’t think too hard about it, it’s just an example of some kind of side effect).track
prop changes (replaced with a new track) we remove the listener from the previous track and add one on the new track.This is what I’ve come up with for Svelte (please let me know if you know a better way):
The implementation of useEffect
is very simple:
const useEffect = (subscribe) => ({ subscribe })
Our useEffect
function returns a custom store. Stores in Svelte should implement a subscribe
method.
The subscribe
method is passed an onChange
function to alert Svelte about new data, and it can return a cleanup function.
Since we’re only interested in side effects we don’t use the onChange
function in our use case.
Svelte has support for creating stores reactively. Svelte also has support for auto subscribing to a store using $
.
So we can use this custom store creating function like so:
effect
is assigned using a reactive declaration, so a new store is created whenever track
is changed.
The Svelte compiler does the dependency tracking work for us, so there’s no dependency array like in React.
Using another reactive statement with $effect
we have Svelte automatically handle subscribing and unsubscribing to the store, without this statement the store will not do anything.
With this we have replicated the most important behaviors of useEffect
.
This implementation isn’t too bad compared to the React version. I’d still be interested in Svelte making things easier so I’ve opened a feature request for Reactive statement cleanup functions.
Things you should never have in your JavaScript code:
[
.(
.return
s.;
.For more info see Semicolons in JavaScript, what are they good for?.
const semicolon = 'clearly terminated';const noSemicolon = 'possibly unfinished'
Some people enjoy typing out an extra character on every line.
It goes without saying you’ll also get to read slightly more.
This attribute of semicolons will help you keep code reviewers on their toes.
Semicolons | No Semicolons |
---|---|
|
|
This change now shows as affecting two lines, when it really only affects one. I bet our reviewer didn’t see that one coming when they spent time scanning both lines for changes ;).
Semicolons | No Semicolons |
---|---|
|
|
This change now shows as affecting two lines, when it really only affects one.
Semicolons | No Semicolons |
---|---|
Shuffling lines is easy, especially if you use vim. If you’d like to swap the last and second last chained methods you need to manually fix the semicolon. This ensures you don’t accidentally modify your code.
// Something a civilized person might do, creating a variable.const myNumbers = [1, 2, 3][1, 2, 3].forEach() // I didn't make a variable for these.// I don't even know what they are ¯\_(ツ)_/¯.// I also don't use prettier, which would pull this line up making my mistake very obvious.
OH NO, A BUG CAUSED BY LACK OF SEMICOLONS! Surely it will take us hours to figure out what’s wrong.
Do you write code like this? If you do you’ll love semicolons.
If you like making it needlessly more difficult to type out, modify, and code review, your code, then you should use semicolons in JavaScript.
For more info see Four Things to Avoid in JavaScript.
Migrating a large project to TypeScript even with the loosest settings may be insurmountable. The number of errors is overwhelming.
You may think the best options for fixing this situation is to use a mix of TypeScript and JS (using the allowJs
flag), however there is a risk of a large amount of code never being typed because there is not enough incentive and feedback to do so.
If you run TypeScript on a large project after renaming files you may be faced with something like:
Or there could be a lot more errors, this project started with 15k errors.
Unfortunately for a project of any sufficient size, you’re going to run into trouble trying to migrate to TypeScript in one go.
So what are your options?
Dimensions of Comparison | Solo Hackathon | Team Hackathon | Enable on Changed Files | Checklist + Coach Team | Snapshot Test |
---|---|---|---|---|---|
Fast | ✅ | ✅ | ❌ | ❌ | ❌ |
High Quality Results | ✅ | YMMV | ✅ | ✅ | ✅ |
Low Team-Coordination | ✅ | ❌ | ✅ | ❌ | ✅ |
Non-Breaking For Wip | ❌ | ❌ | ✅ | ✅ | ✅ |
Reliable On Larger Repos/Messy Code | ❌ | ❌ | ✅ | ✅ | ✅ |
Can Enable Strict Rules On Day 1 | ❌ | ❌ | ✅ | ✅ | ✅ |
Easy To Add Stricter Rules | ❌ | ❌ | ✅ | ✅ | ✅ |
Will Achieve 0 Errors | ✅ | ✅ | ❌ | ✅ | ✅ |
Easily Repeatable For New Rules | ❌ | ❌ | ❌ | ✅ | ✅ |
Think about it.
Ideally you would like to achieve 0 errors and have an easily repeatable process for preventing new errors. To do this your best option may be creating a snapshot test.
On a high-level, using a snapshot test requires creating a test that runs TypeScript and saves a snapshot of all the errors along with filenames and line numbers. This test will fail anytime lines are shifted on a file with errors, or when new errors are added to the codebase — this serves as a reminder to fix type errors when adding or modifying code. This requires low coordination because it’s an automated approach.
It also becomes very easy to incrementally increase the strictness of type checking, the incremental approach is the same.
In essence, the snapshot test is closer to the code than any checklist process and it requires low team-coordination.
This repo (DylanVann/typescript-migration-demo) shows a basic example of how to snapshot test TypeScript errors.
Here’s how it works, consider the following 3 untyped JS files:
// add.jsexport default function add(a, b) {a + b}
// subtract.jsexport default function subtract(a, b) {a - b}
// example.jsimport add from './add'import subtract from './subtract'add('1', 3, 'hello world')subtract('1', 3, 'hello world')
When we convert to TypeScript (changing file extensions and adding a tsconfig.json
file) this will produce a number of type errors:
At this point you should run the snapshot test and commit the result. The snapshot of the errors will look something like this:
When you fix type errors you can run yarn check-ts -u
to update the snapshot, and you will commit something like this:
If you were to add a type error by accident you would see something like this:
So at this point if you are doing PR reviews your reviewer would probably reject this change.
This technique applies to any pattern that can be detected using code quality tools. For example it’s possible to write ESLint rules for bad practices specific to your codebase. You can then incrementally remove them using this technique.
Out of all the possible techniques to migrate to TypeScript this one has a lot of things going for it.