Timer: The React Experience And Conclusions
Tags: react, webdev, tutorial, svelte
Published: 2023-09-07 19:58:16

Welcome, everyone, to the conclusion of this small series. We will be creating a **React** timer and comparing it to what we did in **Svelte** in the first article of the series.

> Note that I have decided not to animate this timer. I ran a few searches about how to animate using **React** and the learning curve is daunting in comparison to learning to animate in **Svelte**. I just don't want to waste time learning something I know I won't be using.

We will do this one quickly (hopefully) since we already know how to make the timer and what we will need.

## Step 1: Basic Markup, Styling and Number Formatting

Let's copy the formatting function over from the previous article, and return a simplified markup since we won't be animating.

Note that the variables `hh`, `mm` and `ss` are now *states* as per **React**'s requirement. Otherwise, the UI won't update.

```typescript
import { useState } from 'react';

export type TimerProps = {
  countFrom?: number;
};

function f(value) {
  if (value < 10) {
    return `0${value}`;
  }
  return value.toString();
}

function Timer({ countFrom = 0 }: TimerProps) {
  const [hh, setHh] = useState(countFrom);
  const [mm, setMm] = useState(countFrom);
  const [ss, setSs] = useState(countFrom);

  return (
    <span className="timer">
      {f(hh)}:{f(mm)}:{f(ss)}
    </span>
  );
}

export default Timer;
```

This produces a Timer component that shows up as `00:00:00`.

## Step 2: Counting Functionality

As far as I can tell, **React** doesn't have an equivalent to ***Svelte*** *stores*, and I won't be using Redux for a timer, so we'll implement the functionality the best way I know without using external libraries.

Our algorithm requires a reactive `endDate` value that changes whenever `countFrom` changes. So let's start with that:

```typescript
// Outside the Timer function:
function addToDate(secs: number) {
  const c = Date.now() + secs * 1000;
  return new Date(c);
}

  // ... Inside the Timer function:
  const [endDate, setEndDate] = useState(addToDate(countFrom));

  useEffect(() => {
    setEndDate(addToDate(countFrom));
  }, [countFrom]);
```

Now, we want to set a new timer every time `endDate` changes, so we add another `useEffect()` call that depends on `endDate`:

```typescript
  const [remaining, setRemaining] = useState(countFrom);

  useEffect(() => {
    setEndDate(addToDate(countFrom));
  }, [countFrom]);

  useEffect(() => {
    const interval = setInterval(() => {
      let r = Math.round((endDate.getTime() - new Date().getTime()) / 1000);
      r = Math.max(r, 0);
      setRemaining(r);
      if (r === 0) {
        clearInterval(interval);
      }
    }, 1000);

    return () => clearInterval(interval);
  }, [endDate]);
```

Ok, this one is a bit more complex. We created another state for the remaining seconds, which is the equivalent (I hope) of the readable **Svelte** store in the previous article. The timer callback kills the timer if zero has been reached, and the effect's cleanup function also clears the timer.

> At this point, my **React** skills seem to not be good enough: It seems to me that I should have another `clearInterval()` call to kill the previous timer whenever `endDate` changes. So any experts out there: Correct me on this one, if you please.

Next, we code another `useEffect()` call depending on the value of `remaining`, that will calculate the final values we are after:

```typescript
  useEffect(() => {
    const hours = Math.floor(remaining / 3600);
    const minutes = Math.floor((remaining - hours * 3600) / 60);
    const seconds = remaining - hours * 3600 - minutes * 60;
    setHh(hours);
    setMm(minutes);
    setSs(seconds);
  }, [remaining]);
```

Note the use of extra variables here: We cannot base the calculations on the state variables because they are updated in the next cycle and would therefore be out of sync by the time this effect runs.

> If we were to use `hh` and `mm` here, we would see `-1` in the seconds part of the timer on the turn of every minute.

With this in place, we can test. What do we see? Well, we see that the timer works, but the initial value is never shown. If we set `countFrom` to, say, 20 seconds, we see the first update to be 19 seconds. The **Svelte** timer has no trouble showing the initial value immediately.

Fixing this requires us to calculate and set the display values in the `useEffect()` for `endDate`. This will cover the initial value. The timer will cover the rest. This is the entire `Timer.tsx` file with the modification:

```typescript
import { useEffect, useState } from 'react';

export type TimerProps = {
  countFrom?: number;
};

function f(value) {
  if (value < 10) {
    return `0${value}`;
  }
  return value.toString();
}

function addToDate(secs: number) {
  const c = Date.now() + secs * 1000;
  return new Date(c);
}

function Timer({ countFrom = 0 }: TimerProps) {
  const [hh, setHh] = useState(countFrom);
  const [mm, setMm] = useState(countFrom);
  const [ss, setSs] = useState(countFrom);
  const [endDate, setEndDate] = useState(addToDate(countFrom));

  function calculateRemainingTime() {
    const r = Math.round((endDate.getTime() - new Date().getTime()) / 1000);
    return Math.max(r, 0);
  }

  useEffect(() => {
    setEndDate(addToDate(countFrom));
  }, [countFrom]);

  useEffect(() => {
    updateTimeParts(calculateRemainingTime());
    const interval = setInterval(() => {
      let r = calculateRemainingTime();
      updateTimeParts(r);
      if (r === 0) {
        clearInterval(interval);
      }
    }, 1000);

    return () => clearInterval(interval);
  }, [endDate]);

  function updateTimeParts(x: number) {
    const hours = Math.floor(x / 3600);
    const minutes = Math.floor((x - hours * 3600) / 60);
    const seconds = x - hours * 3600 - minutes * 60;
    setHh(hours);
    setMm(minutes);
    setSs(seconds);
  }

  return (
    <span className="timer">
      {f(hh)}:{f(mm)}:{f(ss)}
    </span>
  );
}

export default Timer;
```

We have created a new function, `calculateRemainingTime()` to encapsulate the remaining time calculation because we are not copy/paste troglodytes. This change frees up the `remaining` state. We have removed it.

Test this. It shows the initial value now. The final touch is the `timesup` event. Well, not really an event because there are no events in **React**, but a callback.

We need to do 3 updates:

1. Update the `props` type.
    
2. Add the new prop to the `Timer` function's parameters.
    
3. Call the callback once the timer reaches zero.
    

Once more, here is `Timer.tsx` with these modifications:

```typescript
import { useEffect, useState } from 'react';

export type TimerProps = {
  countFrom?: number;
  onTimesup?: () => void
};

function f(value) {
  if (value < 10) {
    return `0${value}`;
  }
  return value.toString();
}

function addToDate(secs: number) {
  const c = Date.now() + secs * 1000;
  return new Date(c);
}

function Timer({ countFrom = 0, onTimesup }: TimerProps) {
  const [hh, setHh] = useState(countFrom);
  const [mm, setMm] = useState(countFrom);
  const [ss, setSs] = useState(countFrom);
  const [endDate, setEndDate] = useState(addToDate(countFrom));

  function calculateRemainingTime() {
    const r = Math.round((endDate.getTime() - new Date().getTime()) / 1000);
    return Math.max(r, 0);
  }

  useEffect(() => {
    setEndDate(addToDate(countFrom));
  }, [countFrom]);

  useEffect(() => {
    updateTimeParts(calculateRemainingTime());
    const interval = setInterval(() => {
      console.log('Tick from interval ID %d !!', interval);
      let r = calculateRemainingTime();
      updateTimeParts(r);
      if (r === 0) {
        clearInterval(interval);
        (onTimesup ?? (() => {}))();
      }
    }, 1000);

    return () => clearInterval(interval);
  }, [endDate]);

  function updateTimeParts(x: number) {
    const hours = Math.floor(x / 3600);
    const minutes = Math.floor((x - hours * 3600) / 60);
    const seconds = x - hours * 3600 - minutes * 60;
    setHh(hours);
    setMm(minutes);
    setSs(seconds);
  }

  return (
    <span className="timer">
      {f(hh)}:{f(mm)}:{f(ss)}
    </span>
  );
}

export default Timer;
```

Ok, this has been harder than the **Svelte** version. Is it done? No. I have tested it and at this point there are two problems:

1. The `timesup` callback fires when my test page loads, before setting a value for `countFrom`.
    
2. The timer shows as `NaN:NaN:NaN` whenever the input box that I created to set `countFrom` values is blanked out.
    

The first one is also revealing something: The timer is firing once even when there is no count set up. These 2 are fixed by means of an IF statement:

```typescript
  useEffect(() => {
    updateTimeParts(calculateRemainingTime());
    let interval = 0;
    if (countFrom > 0) {
      interval = setInterval(() => {
        console.log('Tick from interval ID %d !!', interval);
        let r = calculateRemainingTime();
        updateTimeParts(r);
        if (r === 0) {
          clearInterval(interval);
          (onTimesup ?? (() => {}))();
        }
      }, 1000);
    }

    return () => clearInterval(interval);
  }, [endDate]);
```

To fix #2, we need to understand the problem. For this, I have logged the value and type of `countFrom` to the console: Whenever the input box is blanked out, `countFrom` remains of type `number`, but its value is `NaN`.

Therefore, one possible fix is to replace `countFrom` whenever it is `NaN` with a zero in any calculation.

The fix looks like this:

```typescript
  function safeCountFrom() {
    return isNaN(countFrom) ? 0 : countFrom;
  }
  
  const [hh, setHh] = useState(safeCountFrom());
  const [mm, setMm] = useState(safeCountFrom());
  const [ss, setSs] = useState(safeCountFrom());
  const [endDate, setEndDate] = useState(addToDate(safeCountFrom()));

  // Later in the file:
  useEffect(() => {
    setEndDate(addToDate(safeCountFrom()));
  }, [countFrom]);
```

We created the `safeCountFrom()` function that replaces `NaN` with a zero. We use it whenever we initialize state, and whenever we calculate `endDate`.

Now we are truly done. We have reached **Milestone 1**. Let's count lines: `72 total lines - 12 blank lines = 60 lines of code`.

## Svelte vs React Comparison

Okay, so time to compare. Let's talk about:

1. The net results.
    
2. The time invested.
    
3. The issues encountered while developing the timer.
    

### Net Result

We reached an equivalent timer in **React** with a total line count of **60**. In our previous article, we reached this point with **Svelte** in **42** lines. **React** needed **18** additional lines of code. Doesn't sound like much, right? Wrong. We are talking about a **43%** increase in code when we use **Svelte** as the reference, or a **30%** reduction when **React** is the reference.

Now this was only one component. How many components are there in your project(s)? Most medium-sized projects have around 15 to 25 components. You do your math.

We did not venture into **Milestone 2** (animated timer) in **React**, but in **Svelte**, we accomplished this with **11** extra lines, bumping the **Svelte** source code to **53** lines. This is still less than **Milestone 1** in **React**.

### Time Invested

I certainly invested more time in the **React** version because its syntax and mechanics are more complex: We must use state everywhere, we must use the effect hook to calculate derived values, and we certainly spent time with issues that the **Svelte** counterpart never presented in the first place.

In terms of time invested in learning both technologies, I can tell you that I came to know **React** about 3 and a half years ago, and I came to know **Svelte** about 4 or 5 months ago. I have tried to master **React** 3 times now, by trying to finish a 400-lesson course at udemy.com. I have never gone past lesson 100. On the other hand, I learned 100% of **Svelte** in one day.

### Issues

Reviewing the **Svelte** article, I documented one problem while developing: I needed to re-create the readable store on changes in `countFrom`.

This article, on the other hand, documented these problems while developing:

1. The timer was not showing the initial `countFrom` value.
    
2. The `timesup` callback was firing on component creation.
    
3. The timer was showing `NaN` values whenever the `countFrom` value was `NaN`.
    

> Note that this happened even after gaining the experience while doing the **Svelte** timer, which provided us with the timer algorithm. We did not have to think about the implementation; we just needed to port it over to **React**. I truly expected to finish it faster.

## Conclusions

The code reduction that **Svelte** promises is very real. It is not insignificant by any standard and should be of great help when maintaining large code bases.

**Svelte**'s more natural way of programming using reactive variables and reactive blocks works very well and seems to be more resilient to frontier cases. The **Svelte** timer doesn't suffer when the bound input box is blanked out, and doesn't fire the `timesup` event on component creation, all of it without us having to actually think about this.

Since **Svelte** doesn't require hooks or state and instead its compiler is smart enough to make variables reactive, looking at its codebase is far more pleasant than looking at the **React** codebase. This should translate into easier maintenance, and it should enable programmers with less experience or expertise to successfully work with it more often than working with **React**.

---

So, this was not fun. I think **Svelte** surpasses the #1 in the world in every category in the *Developer Experience* front. If you were to ask me, I would say **Svelte** came to destroy every other competitor out there.

If you haven't learned **Svelte**, then I recommend that you do it now. It only takes one day. [Learn Svelte](https://learn.svelte.dev). Happy coding!

---

Just to comply, here's a live demo of the **React** timer we developed here:

{% embed https://stackblitz.com/edit/stackblitz-starters-wzdhqk?embed=1&view=preview %}