Skip to content

Commit

Permalink
feat: add useCountDown hook (#49)
Browse files Browse the repository at this point in the history
The useCountdown hook is useful for creating a countdown timer.
  • Loading branch information
immois authored Aug 11, 2023
1 parent 2aa3fac commit 4ffccdb
Show file tree
Hide file tree
Showing 6 changed files with 7,271 additions and 2,427 deletions.
5 changes: 5 additions & 0 deletions .changeset/fast-boats-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@raddix/use-count-down': minor
---

Added the useCountDown hook
45 changes: 45 additions & 0 deletions packages/utilities/use-count-down/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "@raddix/use-count-down",
"description": "The useCountdown hook is useful for creating a very simple yet powerful countdown timer for React.",
"version": "0.0.0",
"license": "MIT",
"main": "src/index.ts",
"author": "Moises Machuca Valverde <rolan.machuca@gmail.com> (https://www.moisesmachuca.com)",
"homepage": "https://www.raddix.website",
"repository": {
"type": "git",
"url": "https://github.com/gdvu/raddix.git"
},
"keywords": [
"react-timer",
"react-use-count-down",
"react-count-down",
"hook-count-down",
"react-countdown-hook"
],
"sideEffects": false,
"scripts": {
"lint": "eslint \"{src,tests}/*.{ts,tsx,css}\"",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
"build": "tsup src --dts",
"prepack": "clean-package",
"postpack": "clean-package restore"
},
"files": [
"dist",
"README.md"
],
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
},
"clean-package": "../../../clean-package.config.json",
"tsup": {
"clean": true,
"target": "es2019",
"format": [
"cjs",
"esm"
]
}
}
85 changes: 85 additions & 0 deletions packages/utilities/use-count-down/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { useState, useEffect, useMemo, useRef, useCallback } from 'react';

interface Options {
autoStart?: boolean;
/** A callback function to be called when the countdown reaches zero. */
onFinished?: () => void;
/** A callback function to be called on each specified interval of the countdown. */
onTick?: () => void;
}

interface CountDownResult {
readonly value: number;
readonly stop: () => void;
// readonly trigger: () => void;
readonly reset: () => void;
readonly isFinished: boolean;
}

type UseCountDown = (
initialValue: number,
interval: number,
options?: Options
) => CountDownResult;

// the new time is accessed and subtracted from the countdown.
const calc = (time: number) => time - Date.now();

export const useCountDown: UseCountDown = (
initialValue,
interval = 1000,
options = {}
) => {
const { autoStart = true, onFinished, onTick } = options;
const timeLeft = useMemo(() => Date.now() + initialValue, [initialValue]);

const [timer, setTimer] = useState<number>(calc(timeLeft));
const [isFinished, setIsFinished] = useState<boolean>(false);
const timerRef = useRef<NodeJS.Timer | null>(null);

const stop = useCallback(() => {
if (!timerRef.current) return;

clearInterval(timerRef.current);
timerRef.current = null;
}, []);

const trigger = useCallback(() => {
if (timerRef.current) return;
// if (isFinished) return;

timerRef.current = setInterval(() => {
const targetLeft = calc(timeLeft);
setTimer(targetLeft);
if (onTick) onTick();

if (targetLeft === 0) {
stop();
setIsFinished(true);
if (onFinished) onFinished();
}
}, interval);
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [timeLeft, interval, stop]);

const reset = useCallback(() => {
setTimer(initialValue);
setIsFinished(true);
}, [initialValue]);

useEffect(() => {
if (autoStart) trigger();

return () => {
stop();
};
}, [stop, trigger, autoStart]);

return {
value: timer,
stop,
// trigger,
reset,
isFinished: isFinished
};
};
96 changes: 96 additions & 0 deletions packages/utilities/use-count-down/tests/use-count-down.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { useCountDown } from '@raddix/use-count-down';
import { renderHook, act } from '@testing-library/react';

jest.useFakeTimers();

// afterEach(() => {
// jest.runOnlyPendingTimers();
// jest.useRealTimers();
// });

describe('useCountDown test:', () => {
test('should print initial values', () => {
const { result } = renderHook(() => useCountDown(4000, 1000));
const { value, isFinished, stop, reset } = result.current;

expect(value).toBe(4000);
expect(typeof stop).toBe('function');
// expect(typeof trigger).toBe('function');
expect(typeof reset).toBe('function');
expect(isFinished).toBe(false);
});

test('should decrement count', () => {
const initialTime = 60 * 1000;
const { result } = renderHook(() => useCountDown(initialTime, 500));

act(() => {
jest.advanceTimersByTime(1000);
});

expect(result.current.value).toBe(59000);
});

test('should start timer and stop on 0', () => {
const initialTime = 10 * 1000;
const { result } = renderHook(() => useCountDown(initialTime, 1000));

act(() => {
jest.advanceTimersByTime(1000); // Advance the first tick
});

// Now, use 'act' again to wait for the interval to complete
act(() => {
jest.advanceTimersByTime(9000); // Advance the remaining time
});

expect(result.current.value).toBe(0);
expect(result.current.isFinished).toBe(true);
});

test('the timer should stop', () => {
const initialTime = 15 * 1000;
const { result } = renderHook(() => useCountDown(initialTime, 1000));

act(() => {
jest.advanceTimersByTime(1000);
});

expect(result.current.value).toBe(14000);

act(() => {
result.current.stop();
jest.advanceTimersByTime(2000);
});

expect(result.current.value).toBe(14000);
});

test('the onTick function should be called at every interval', () => {
const onTick = jest.fn();
renderHook(() => useCountDown(10000, 500, { onTick }));

act(() => {
jest.advanceTimersByTime(5000);
});

expect(onTick).toBeCalledTimes(10);
});

test('the onFinished function should be called when the countdown reaches zero', () => {
const onFinished = jest.fn();
renderHook(() => useCountDown(5000, 500, { onFinished }));

act(() => {
jest.advanceTimersByTime(2000);
});

expect(onFinished).not.toBeCalled();

act(() => {
jest.advanceTimersByTime(3000);
});

expect(onFinished).toBeCalled();
});
});
Loading

0 comments on commit 4ffccdb

Please sign in to comment.