Understanding Svelte – Reactivity (part 1)
Last time I wrote about the basics of Svelte and Svelte components. At the end we rendered a "crypto tab" on screen that displayed a static price. So in this post, let's fetch the actual spot price of Bitcoin and display it in a nicely formatted way. In the process, we might learn a thing or two about reactivity, one of the core concepts in Svelte.
Fetch the price
Our component currently has the following markup:
// src/CryptoCard.svelte
<div class="card">
<label>BTC</label>
<div class="icon-holder">
<IconRefresh fill="#CBD5E0" stroke="#CBD5E0" strokeWidth="0.1"/>
</div>
<span>$10,209</span>
</div>
We'll replace the hard-wired price with the current price. In Svelte templates, anything enclosed in curly braces is dynamic:
<div class="card">
<!-- (...) -->
<span>${price}</span>
</div>
Let's first grab that price from the Coinbase API:
<script>
let price = 'N/A';
async function updatePrice() {
let priceInfo = await fetch(`https://api.coinbase.com/v2/prices/BTC-USD/spot`).then(r => r.json());
price = priceInfo.data.amount;
}
</script>
Reactive assignments
That code is not run yet, though, so we'll have to find a way to do so when our component is rendered. Svelte provides an easy way to do this: the onMount
hook.
<script>
import { onMount } from 'svelte';
// (...)
onMount(updatePrice);
</script>
Svelte has "reactive assignments". As we display the price
variable directly in the template, and its value has changed, Svelte updates its value in the DOM.
Our tab now shows us the real price (don't mind that the price is different from last time):
The raw price has too much precision, so let's round it:
<script>
let decimals = 0;
function roundPrice() {
let mult = 10 ** decimals;
return Math.round(+price * mult) / mult;
}
</script>
<div class="card">
<!-- (...) -->
<span>${roundPrice()}</span>
</div>
That should be all, right?
Reactive declarations
We're in for a bit of a surprise, the price is now the infamous NaN:
The price we display is a the return value of calling roundPrice
and is executed when the component renders. However, the price
variable still has it's initial value, N/A
which cannot be rounded.
So that explains the NaN
but why doesn't it get updated after the fetch completes and the price is set?
The roundPrice
function is not called again because Svelte has no way of knowing it should be. We have to make the displayed variable reactive – to re-render when its dependencies change.
The way to create a reactive variable that depends on the value of other variables is to use a "reactive declaration", using $:
(which is, to my surprise, valid JavaScript, a labelled statement):
<script>
$: roundedPrice = roundPrice();
</script>
<div class="card">
<!-- (...) -->
<span>${roundedPrice}</span>
</div>
We still have the $NaN
, though. Why? The way we declared roundedPrice
, we didn't specify which other variables it depends on. That knowledge is implicit in the roundPrice
function. Let's refactor that function so that we can make the dependencies explicit in the declaration of roundedPrice
.
<script>
function round(v, decimals) {
let mult = 10 ** decimals;
return Math.round(+v * mult) / mult;
}
$: roundedPrice = round(price, 0);
</script>
Functioning. roundedPrice
clearly depends on price
so when price
changes, the declaration gets executed again and roundedPrice
receives its new value.
Let's wrap up by also formatting the price so it's easier to read. We'll send through the rounded price to a formatPrice
function:
<script>
$: formattedPrice = formatPrice(round(price, 0));
function round(v, decimals) {
let mult = 10 ** decimals;
return Math.round(+v * mult) / mult;
}
function formatPrice(rawPrice) {
if (!rawPrice) {
return 'N/A';
}
let p = rawPrice.toString().split('').reverse();
let i = 0;
let displayed = [];
while (i < p.length) {
displayed.push(p.slice(i, i+3).reverse().join(''));
i += 3;
}
return displayed.reverse().join(',');
}
</script>
<div class="card">
<!-- (...) -->
<span>${formattedPrice}</span>
</div>
Conceptually, there's nothing new here: the fact that our reactive declaration now has two nested function calls doesn't change things. Our code keeps working and the price gets updated when the price information is grabbed:
That concludes this post. We'll probably look at how to fetch and display historical prices next – maybe there's something to learn there, too.