UseCountdown

function

Hooks pass in a string of the deadline date and the moment format string for the date, and return the remaining days, hours, minutes, and seconds

The effect

Thinking process

TS, where the input and return values are written first:

/ / into the refs
export interface ICountdown {
  deadline: string; // Termination dateformat? :'YYYY-MM-DD HH:mm:ss' | string;
}
/ / the return value
export type Remains = Record<'day' | 'hour' | 'minute' | 'second'.number>;
Copy the code

Before you start writing a method body, think about what states you need:

First, you need a constant update of the current time (you can initialize it by using moment to get the current time). Once you get the current time, set a timer that updates every second.

Second, the remaining time is calculated by comparing the update time per second with the deadline passed in by the user.

So we need two states: current and remains.

Current Saves the current time. Remains holds the calculated time and returns it to the user.

const [current, setCurrent] = useState(moment());
const [remains, setRemains] = useState<Remains>({
  day: 0.hour: 0.minute: 0.second: 0});Copy the code

When the deadline changes, a timer is created that runs once every second. If the current time is greater than or equal to the deadline, the timer is cleared; otherwise, the time increases by one second.

useEffect(() = > {
  const timer = window.setInterval(() = > {
    current.isSameOrAfter(moment(deadline, format))
      ? clearInterval(timer)
      : setCurrent(prev= > prev.current.add(1.'s'));
  }, 1000);
  return () = > clearInterval(timer);
}, [deadline]);
Copy the code

When the current time changes, compare current and deadline to calculate the remaining time:

Note: MY initial calculation of remains was stupid, using deadline.valueof () -current.valueof () to get a number of milliseconds, and then passing it back to the moment format. The result is actually a time difference of milliseconds relative to the Unix era, rather than the actual time difference. Finally, divide the seconds, minutes, hours and days manually according to the number of milliseconds

useEffect(() = > {
  let millisec = moment(deadline, format).valueOf() - current.valueOf();
  setRemains({
    day: Math.floor(millisec / (1000 * 60 * 60 * 24)),
    hour: Math.floor((millisec / (1000 * 60 * 60)) % 24),
    minute: Math.floor((millisec / (1000 * 60)) % 60),
    second: Math.round((millisec / 1000) % 60)}); }, [current]);Copy the code

Add (1, ‘s’), then update the current object on the basis of the original object. The reference relationship remains unchanged. UseEffect does not sense current changes and cannot trigger hooks.

Therefore, I change current to the form of current + updater. After each update of updater, +1, useEffect senses the change of updater.

const [{ current, updater }, setCurrent] = useState({
  current: moment(),
  updater: 0});Copy the code

The final code is as follows:

import { useEffect, useState } from 'react';
import moment from 'moment';

/ / into the refs
export interface ICountdown {
  deadline: string; format? :'YYYY-MM-DD HH:mm:ss' | string;
}
/ / the return value
export type Remains = Record<'day' | 'hour' | 'minute' | 'second'.number>;

const useCountdown = ({
  deadline,
  format = 'YYYY-MM-DD HH:mm:ss',
}: ICountdown): Remains= > {
  // Because moment() returns an object, setCurrent cannot capture the change in useEffect, so we define a updater to capture the change in useEffect
  const [{ current, updater }, setCurrent] = useState({
    current: moment(),
    updater: 0});const [remains, setRemains] = useState<Remains>({
    day: 0.hour: 0.minute: 0.second: 0}); useEffect(() = > {
    const timer = window.setInterval(() = > {
      current.isSameOrAfter(moment(deadline, format))
        ? clearInterval(timer)
        : setCurrent(prev= > ({
            current: prev.current.add(1.'s'),
            updater: prev.updater + 1,})); },1000);
    return () = > clearInterval(timer);
  }, [deadline]);

  // current change, calculate how much time difference
  useEffect(() = > {
    let millisec = moment(deadline, format).valueOf() - current.valueOf();
    // handle [s] + [s] + [s]
    millisec = millisec >= 0 ? millisec : 0;
    // Get seconds, minutes, hours, and days in milliseconds
    setRemains({
      day: Math.floor(millisec / (1000 * 60 * 60 * 24)),
      hour: Math.floor((millisec / (1000 * 60 * 60)) % 24),
      minute: Math.floor((millisec / (1000 * 60)) % 60),
      second: Math.round((millisec / 1000) % 60)}); }, [updater]);return remains;
};

export default useCountdown;
Copy the code

You can either pass in a string and format directly, or use useMemo() to cache the result of the moment format. (If not cached, useCountdown returns a value update that triggers the Index function. Each time the function is released, a new deadline value is generated.)

// countdown
import React, { useMemo } from 'react';
import useCountdown from '@/components/Countdown';
import moment from 'moment';

const Index = () = > {
  const deadline = useMemo(
    () = >
      moment()
        .add(4.'s')
        .add(1.'m')
        .add(1.'h')
        .add(1.'d')
        .format('YYYY-MM-DD HH:mm:ss'), []);const { day, hour, minute, second } = useCountdown({
    // deadline: '2021-04-22 17:49:00',
    deadline,
  });

  return <h1>{' ${day} ${hour} ${minute} minutes ${second} seconds'}</h1>;
};

export default Index;
Copy the code