Mark Jaquith

Slim Your Svelte

April 27, 2020

Svelte 3 is a magical framework. It gives you all the control of something like React, while producing code bundles a fraction of the size. Beyond reducing bundle size, Svelte 3 helps you write less code in the first place, by minimizing boilerplate. Still, I see a lot of Svelte 3 apps and tutorials that only conservatively reduce the amount of written code.

Here’s an example from CarbonFive:

<script>
let count = 0
$: remaining = 10 - count

const increment = () => {
	if (remaining > 0) {
		count++
	}
}
</script>

<div>
	<div>Count: {count}</div>
	<div>Remaining: {remaining}</div>
	<button on:click={increment}>Click</button>
</div>

Only 16 lines. That’s certainly less code than their React version of the same counter app, with 21 lines:

import React, { useState } from 'react'

const CountDown = () => {
	const [count, setCount] = useState(0)

	const remaining = 10 - count

	const increment = () => {
		if (remaining > 0) {
			setCount(count + 1)
		}
	}

	return (
		<div>
			<div>Count: {count}</div>
			<div>Remaining: {remaining}</div>
			<button onClick={increment}>Click</button>
		</div>
	)
}

Still, I think we can do better. Let’s slim this Svelte.

As we do that, we should keep our objective in mind. “Maintain functionality in the absolute minimum of code” isn’t it. Generally the less code you write, the better. But the computer isn’t the only thing parsing the code. You are parsing it with your mind. Future versions of yourself will be parsing this code with your mind. Other people may be parsing this code with their minds.

The first thing to consider is the wrapping <div /> around the whole component. In React, because JSX gets transpiled to JavaScript objects, you have to return a single node, so a <div /> or React.Fragment wrapper is necessary. Svelte doesn’t use JSX, so this isn’t needed. And you probably don’t want it — leaving it unwrapped leaves parent components with more styling options.

Let’s get rid of that, saving us two lines and one level of HTML indentation:

<script>
let count = 0
$: remaining = 10 - count

const increment = () => {
	if (remaining > 0) {
		count++
	}
}
</script>

<div>Count: {count}</div>
<div>Remaining: {remaining}</div>
<button on:click={increment}>Click</button>

A big chunk of the code is the increment() function — five lines, another blank line, and two indentation levels.

In situations where you are doing complex data manipulations you would be well served to write a handler function like this. But when you’re just doing simple manipulation, it’s overkill. I see people writing a function like disableUpdates() instead of simply writing canUpdate = false. Svelte 3 reacts to simple assignments, and we can use that to write less code.

Let’s change the click handler to directly increment count, and remove increment() entirely.

<button on:click={() => count++}>Click</button>

But what about the upper bound on count? The original code kept count from going beyond 10. Let’s think about this in a Svelte way: whenever count changes, we want to make sure it isn’t more than 10. In other words, we want to react to changes in count by constraining count.

We can do that with Svelte 3’s reactive declarations, which re-run statements whenever any variables within them are changed. Like this:

$: count = Math.min(10, count)

We’ve removed the if condition, and are just letting Math.min() handle that. And now, no matter where count is changed, we know this constraint will be applied to it. This is absolutely a maintenance improvement. Before, we were applying the constraint in a specific updating function. If a second button had been added and used its own logic for updating count, our constraint would no longer apply.

This pattern of using reactive declarations to constrain a variable is very powerful. Let’s say we wanted to enforce a maximum length for a text field.

$: subject = subject.slice(0, 128)

Done. Now our input change handler doesn’t need to do anything except set subject to the new value.

Okay. Back to our counter code, which now looks like this:

<script>
let count = 0
$: remaining = 10 - count
$: count = Math.min(10, count)
</script>

<div>Count: {count}</div>
<div>Remaining: {remaining}</div>
<button on:click={() => count++}>Click</button>

The initialization of count is not necessary, because reactive declarations that set a variable will take care of initialization for you. We can thus remove let count = 0 and provide an inline fallback value of 0 inside the reactive count constraint (so we don’t end up passing undefined to Math.min()).

<script>
$: remaining = 10 - count
$: count = Math.min(10, count || 0)
</script>

<div>Count: {count}</div>
<div>Remaining: {remaining}</div>
<button on:click={() => count++}>Click</button>

We are now down to eight lines — half the code we started with! We have also elminated all indentation, and our code is simpler and easier to follow.

We could take the line reduction one step further, and remove remaining and inline 10 - count in the HTML. But that, in my opinion, would be sacrificing too much clarity.

Many people come to Svelte with prior experience with React. I did as well! We’re used to a lot of boilerplate. But once you start thinking in terms of reactivity, you will be delighted at how simple your code can become.