Animated countdown in react with @react-spring/web
Tags: react, countdown, animatedcountdown, reactspring
Published: 2024-02-15 16:04:35

Today I want to show steps to develop an animated countdown in react.

**Demo**: [https://multivoltage.github.io/react-animated-countdown/](https://multivoltage.github.io/react-animated-countdown/)
**Source** [https://github.com/multivoltage/react-animated-countdown](https://github.com/multivoltage/react-animated-countdown)

**Tool used:**
- `vite` for fast development
- `@react-spring/web` for animation part
- `date-fns` for operations with dates.
- `styled-components` for styles elements (no mandatory since basic css-in-js can be used)

**Separation of goals:**
In my experience I learnt that to achive a problem probably I can achive this goal separating into smaller ones. Since we want to "create a count-down which render remaining time starting from a date in the future" we can found 3 main goal:
1. Create an abstract `<Box />` component responsible only to render a label inside and animate itself each time this label change. In this demo we can find 4 `<Box />`
2. Create `<Countdown />` component responsible to render 4 <Box /> providing that the labels
3. Create a function that help us to scorporate `day`,`hours`,`minutes` and `seconds` from a given future date
4. to mix all previous points

> this demo uses css borders in some. So ```react
* {
  box-sizing: border-box!important;
}
``` is used into `index.css`

---

## Create an abstract `<Box />`
As I said before we want to built a reusable component. This component needs only two props:
- `labelPeriod` (string): we'll pass **"day" "hours"** etc.. but we can also pass **"day ramaining"** or **"hours remaining"** or also **"days of waiting"**
- `labelNumber` (number): this will be a value coming from the helper function. Each time this value changes, animation starts.

Since we want an animation where current number will be covered and next number will be show, we need to create two different faces on this component and use a very cool css utility called `transform-style: preserve-3d`. Thanks to this property combined with `transform: rotateX(180deg)` we can have 2 adiacent faces where the backface is rotated and covered by the main face.

Full final code for <Box /> is available on [github](https://github.com/multivoltage/react-animated-countdown/blob/main/src/components/card.tsx) but here I show up my first implementation before:


![basic box](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lx8wnh28doe0i7o0nkz4.png)
```react
export const Box: React.FC<Props> = ({ labelNumber, labelPeriod }) => {
  return (
    <Container>
      <Card className="card">
        <Content>
          <ContentFront>
            <span>{labelNumber}</span>
            <LabelPeriod>{labelPeriod}</LabelPeriod>
          </ContentFront>
          <ContentBack>
            <span>{labelNumber}</span>
            <LabelPeriod>{labelPeriod}</LabelPeriod>
          </ContentBack>
        </Content>
      </Card>
    </Container>
  );
};

```

Animation works thanks `useSpring()` from `@react-spring/web`. 
```react
  const [style,api] = useSpring(
    () => ({
      from: { rotateX: "0deg" },
      to: { rotateX: "180deg" },
      delay: 0,
      reset: true,
    }),
    [labelNumber]
  );
```

This hook returns two fields:
- the first one `style`: This object is a basic `style` object and we can pass that to `<animated.div />` element (provided by `@react-spring/web`).
- the second one `api`: **Not used in this demo**, but the hook returns also an object where you can call `api.start()` in imperative mode.

Like normal hooks, we add `[labelNumber]` as dependency. In this way the animation **depends** on labelNumber. So when `labelNumber` changes, the animation will start again.

> `from` and `to` need to be of same type. So `rotateX` needs to be the same type of `rotateX` inside `to`. In this demo we use a `string` value. Mixing integers (like `rotateX: 0`) and string does not work.

Magic happends when we pass `style` into `animated.div`. Since we use `styled-components` to add basic style we need to create a component like this:
```react
import styled from "styled-components";
import { animated } from "@react-spring/web";

const Content = styled(animated.div)`
  position: absolute;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  perspective: 500px;
`;
```

and then pass `styles` to `<Content />`
```react
    <Container>
      <Card className="card">
        <Content style={style}>
          ....
        </Content>
      </Card>
    </Container>
```
So after that if we setup a basic container like that:
```react
import useTimer from "./hooks/useTimer";
import { Box } from "./components/card2";

export default function App() {
  // use timer start a timer
  // seconds will be update each 1000ms
  const { seconds } = useTimer(1000, () => {});

  return (
    <main>
      <div
        style={{
          width: 400,
        }}
      >
        <Box labelPeriod="day" labelNumber={seconds} />
      </div>
    </main>
  );
}
```
we have our first animation
![animation with bug](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ow9xl87rllqq3s32agd5.gif)

This is pretty cool but we have a small bug. Animation starts correctly when `label` changes but since the react render is fast, user can see the new number even if animation is not ended. That happends because inside `<ContentBack />` we render same value of `<ContentFront />`. We can avoid this behavior using a custom hook called `usePrevious()`. This hooks ([source code here](https://github.com/multivoltage/react-animated-countdown/blob/main/src/hooks/usePrevious.ts)) return us the previous value of a given value. You can use different implementation like [this one](https://usehooks.com/useprevious).

Adding `usePrevious()` we edit code and pass it to `<ContentBack />`:
```react
  const previous = usePrevious(labelNumber);

  return (
    <Container>
      <Card className="card">
        <Content style={props}>
          <ContentFront>
            <span>{labelNumber}</span>
            <LabelPeriod>{labelPeriod}</LabelPeriod>
          </ContentFront>
          <ContentBack>
            <span>{previous}</span>
            <LabelPeriod>{labelPeriod}</LabelPeriod>
          </ContentBack>
        </Content>
      </Card>
    </Container>
  );
```
The result will be the same of demo link (but only for one single `<Box />`).


## Create `<Countdown />` component
This is only a basic wrapper. Since we have `day`, `hours`, `minutes` and `seconds` we setup a component like this:
```react
export default function Countdown({ startDate, endDate }: Props) {
  // We will discuss this works later
  const { days, hours, minutes, seconds } = getRemamingRangeTimes(
    startDate,
    endDate
  );

  return (
    <Container>
      <Box labelNumber={days} labelPeriod="days" />
      <Box labelNumber={hours} labelPeriod="hours" />
      <Box labelNumber={minutes} labelPeriod="minutes" />
      <Box labelNumber={seconds} labelPeriod="seconds" />
    </Container>
  );
}
```

---

## Create a function that help us to scorporate

Now we try to get `days` `hours` etc from a future Date thanks to a library called `date-dns`. Probably we can do same calculation with `window.Date` but a lot of work is necessary.
```react
import {
  addDays,
  differenceInDays,
  differenceInHours,
  differenceInMinutes,
  differenceInSeconds,
} from "date-fns";

export function generateFutureDay() {
  const future = addDays(new Date(), 10);
  return future;
}

export function getRemamingRangeTimes(startDate: Date, endDate: Date) {
  const days = Math.max(0, differenceInDays(endDate, startDate));
  const hours = Math.max(0, differenceInHours(endDate, startDate) % 24);
  const minutes = Math.max(0, differenceInMinutes(endDate, startDate) % 60);
  const seconds = Math.max(0, differenceInSeconds(endDate, startDate) % 60);

  return {
    days,
    hours,
    minutes,
    seconds,
  };
}
```
`getRemamingRangeTimes` function is the core. We use `Math.max` only because if for some reasons we pass as `endDate` a date in the past we do not want that countdown renders negative number :). 

Since `differenceInMinutes` , `differenceInHours` and `differenceInSeconds` return the same time we use `% 24` to calculate the the remaining hours only for the last day. Same logic for minutes.

---

## Mix all previous points
Since we want to trigger animation each seconds, we need to "generate" labelNumbers based on `Date.now()`. So we can write:
```react
import { generateFutureDay } from "./utils";
import useTimer from "./hooks/useTimer";
import { useRef, useState } from "react";
import Countdown from "./components/countdown";

export default function App() {
  const futureDate = useRef(generateFutureDay());
  const [now, setNow] = useState(new Date());

  const {} = useTimer(1000, () => {
    // refresh now
    setNow(new Date());
  });

  return (
    <main>
      <Countdown startDate={now} endDate={futureDate.current} />
    </main>
  );
}
```

Probably my implementation can be better or maybe web offert a `ready-to-use` solution. I started this tutorial after worked on a real **production product** havy inspired by this implementation. 

We should arrive to a result like that:

![final result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kbn68o915ebbbnwnack2.gif)