Dylan Vann

Svelte Components Are Not Resilient

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:

  1. Don’t stop the data flow.
    • Props and state can change, and components should handle those changes whenever they happen.
  2. Always be ready to render.
    • A component shouldn’t break because it’s rendered more or less often.
  3. No component is a singleton.
    • Even if a component is rendered just once, your design will improve if rendering twice doesn’t break it.
  4. 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:

Now that we have a nice PhotoGrid component, let’s write a component for selecting an album:

PhotoGrid With AlbumSelector in 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 in React (not broken):

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:

PhotoGrid with AlbumSelector in 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.

A Svelte Version of useEffect

Using Custom Stores and Reactive Statements

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:

  • We want to add a listener to the stop event on track.
  • That listener will call a stopAllTracks() method on the track (don’t think too hard about it, it’s just an example of some kind of side effect).
  • When the component is unmounted we will remove the listener on the track.
  • When the 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 very important, otherwise our component would be in an invalid state with respect to its props.
    • Not handling the track prop changing could be considered a bug, and it could be difficult to diagnose.
    • Hooks are intended to help prevent this sort of bug in React.
    • You can read about this in Dan Abramov’s article, React as a UI Runtime and in Writing Resilient Components.

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.

Four Things to Avoid in JavaScript

Common code issues when writing JavaScript.

Things you should never have in your JavaScript code:

  1. Statements starting with [.
  2. Statements starting with (.
  3. Newlines directly after returns.
  4. Semicolons - ;.

For more info see Semicolons in JavaScript, what are they good for?.

Semicolons in JavaScript, what are they good for?

Everything you need to know.

Typing More

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.

Harder to Read Diffs

This attribute of semicolons will help you keep code reviewers on their toes.

Harder to Read Diffs When Removing a Chained Method

SemicolonsNo Semicolons
const thing = another
- .map(v => v * 2)
- .filter(v => v / 5);
+ .map(v => v * 2);
const thing = another
.map(v => v * 2)
- .filter(v => v / 5)

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 ;).

Harder to Read Diffs When Adding a Chained Method

SemicolonsNo Semicolons
const thing = another
- .map(v => v * 2);
+ .map(v => v * 2)
+ .filter(v => v / 5);
const thing = another
.map(v => v * 2)
+ .filter(v => v / 5)

This change now shows as affecting two lines, when it really only affects one.

Making It More Difficult to Move Around Lines

SemicolonsNo 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.

Preventing Bugs in Cases That Will Never Come up If the Code You’re Writing Isn’t Terrible

// 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.

Conclusion

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.

How to Incrementally Migrate 100k Lines of Code to Typescript

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:

Too many TypeScript errors.

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 ComparisonSolo HackathonTeam HackathonEnable on Changed FilesChecklist + Coach TeamSnapshot Test
Fast
High Quality ResultsYMMV
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.

How to create a snapshot test of TypeScript errors?

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.js
export default function add(a, b) {
a + b
}
// subtract.js
export default function subtract(a, b) {
a - b
}
// example.js
import 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:

Errors after changing file extensions to .ts.

At this point you should run the snapshot test and commit the result. The snapshot of the errors will look something like this:

Snapshot of TypeScript errors.

What happens when I fix or add type errors?

When you fix type errors you can run yarn check-ts -u to update the snapshot, and you will commit something like this:

Diff after fixing TypeScript errors.

If you were to add a type error by accident you would see something like this:

Diff after adding TypeScript errors.

So at this point if you are doing PR reviews your reviewer would probably reject this change.

Using ESLint (or other tools)

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.

Conclusion

Out of all the possible techniques to migrate to TypeScript this one has a lot of things going for it.

1 of 5
© 2020 Dylan Vann