Understanding Svelte – Reactivity (part 1)

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.

We should've bought some BTC 2 minutes ago

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:

... and then we should've sold it

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.