From dc0cdb82731687ed4a38ffdfa4d9b560d9d3da4e Mon Sep 17 00:00:00 2001 From: ofk0 Date: Sun, 30 Aug 2026 04:27:24 +0300 Subject: [PATCH 1/3] docs: wrap reset handler in useStopwatch example --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 220bc7c..8e285f7 100644 --- a/readme.md +++ b/readme.md @@ -144,7 +144,7 @@ function MyStopwatch() {

{isRunning ? 'Running' : 'Not running'}

- + ); } From 829755e8bd941f7ce0c2b841f9c1da56cb40d154 Mon Sep 17 00:00:00 2001 From: ofk0 Date: Mon, 28 Sep 2026 02:35:55 +0300 Subject: [PATCH 2/3] fix: date check on reset function --- demo/components/UseStopwatchDemo.tsx | 4 ++-- src/useStopwatch.ts | 14 +++++++------- src/utils/Time.ts | 5 +++++ 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/demo/components/UseStopwatchDemo.tsx b/demo/components/UseStopwatchDemo.tsx index 0b05489..bf033b2 100644 --- a/demo/components/UseStopwatchDemo.tsx +++ b/demo/components/UseStopwatchDemo.tsx @@ -3,7 +3,7 @@ import { useStopwatch } from '../../src/index'; import Button from './Button'; import TimerStyled from './TimerStyled'; -export default function UseStopwatchDemo({ interval }: { interval: number}) { +export default function UseStopwatchDemo({ interval }: { interval: number }) { const { milliseconds, seconds, @@ -22,7 +22,7 @@ export default function UseStopwatchDemo({ interval }: { interval: number}) { - + ); } diff --git a/src/useStopwatch.ts b/src/useStopwatch.ts index 13e3ecf..52c5053 100644 --- a/src/useStopwatch.ts +++ b/src/useStopwatch.ts @@ -1,4 +1,4 @@ -import { useState, useCallback } from 'react'; +import { useState, useCallback, SyntheticEvent } from 'react'; import { Time } from './utils'; import { useInterval } from './hooks'; import { SECOND_INTERVAL } from './constants'; @@ -11,14 +11,14 @@ export type useStopwatchSettingsType = { }; export type useStopwatchResultType = TimeFromMillisecondsType & { - start: () => void, - pause: () => void, - reset: (offset?: Date, newAutoStart?: boolean) => void, + start: () => void, + pause: () => void, + reset: (offset?: Date | SyntheticEvent, newAutoStart?: boolean) => void, isRunning: boolean, }; export default function useStopwatch({ autoStart = true, offsetTimestamp, interval: customInterval = SECOND_INTERVAL }: useStopwatchSettingsType = {}): useStopwatchResultType { - const offsetMilliseconds = offsetTimestamp ? Time.getMillisecondsFromExpiry(offsetTimestamp) : 0; + const offsetMilliseconds = Time.isValidDate(offsetTimestamp) ? Time.getMillisecondsFromExpiry(offsetTimestamp) : 0; const [prevTime, setPrevTime] = useState(new Date().getTime() - new Date(offsetMilliseconds).getTime()); const [milliseconds, setMilliseconds] = useState(Time.getMillisecondsFromPrevTime(prevTime || 0)); const [isRunning, setIsRunning] = useState(autoStart); @@ -45,8 +45,8 @@ export default function useStopwatch({ autoStart = true, offsetTimestamp, interv } }, [prevTime, isRunning]); - const reset = useCallback((offset?: Date, newAutoStart = true) => { - const newOffsetMilliseconds = offset ? Time.getMillisecondsFromExpiry(offset) : 0; + const reset = useCallback((offset?: Date | SyntheticEvent, newAutoStart = true) => { + const newOffsetMilliseconds = Time.isValidDate(offset) ? Time.getMillisecondsFromExpiry(offset) : 0; const newPrevTime = new Date().getTime() - new Date(newOffsetMilliseconds).getTime(); const newMilliseconds = Time.getMillisecondsFromPrevTime(newPrevTime); const millisecondsOffset = SECOND_INTERVAL - (newMilliseconds % SECOND_INTERVAL); diff --git a/src/utils/Time.ts b/src/utils/Time.ts index 21d9758..41365e2 100644 --- a/src/utils/Time.ts +++ b/src/utils/Time.ts @@ -40,6 +40,11 @@ export default class Time { }; } + static isValidDate(value: unknown): value is Date { + return Object.prototype.toString.call(value) === '[object Date]' + && !Number.isNaN((value as Date).getTime()); + } + static getMillisecondsFromExpiry(expiry: Date): number { const now = new Date().getTime(); const milliSecondsDistance = expiry?.getTime() - now; From ad589a83b3439e15e9797b490d9218adf801204f Mon Sep 17 00:00:00 2001 From: ofk0 Date: Mon, 28 Sep 2026 02:37:48 +0300 Subject: [PATCH 3/3] test: date check on reset function --- __tests__/useStopwatch.test.ts | 48 ++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/__tests__/useStopwatch.test.ts b/__tests__/useStopwatch.test.ts index e1ff9b6..febca30 100644 --- a/__tests__/useStopwatch.test.ts +++ b/__tests__/useStopwatch.test.ts @@ -83,6 +83,19 @@ describe('useStopwatch', () => { expect(result.current.seconds).toBe(0); expect(result.current.milliseconds).toBe(0); }); + + test('non Date offsetTimestamp values', () => { + const { result } = renderHook(() => + useStopwatch({ offsetTimestamp: {} as unknown as Date }) + ); + + expect(result.current.seconds).toBe(0); + expect(result.current.totalMilliseconds).toBe(0); + + act(() => { jest.advanceTimersByTime(3000); }); + + expect(result.current.seconds).toBe(3); + }); }); describe('interval', () => { @@ -404,5 +417,40 @@ describe('useStopwatch', () => { expect(result.current.isRunning).toBe(true); expect(result.current.seconds).toBe(5); }); + + test('calling reset with a non Date argument resets the stopwatch to 0', () => { + const { result } = renderHook(() => + useStopwatch() + ); + + act(() => { jest.advanceTimersByTime(5000); }); + + expect(result.current.seconds).toBe(5); + + // react passes the event object when reset is used directly as onClick={reset} + act(() => result.current.reset({ type: 'click' } as unknown as Date)); + + expect(result.current.seconds).toBe(0); + + act(() => { jest.advanceTimersByTime(2000); }); + + expect(result.current.isRunning).toBe(true); + expect(result.current.seconds).toBe(2); + }); + + test('calling reset with an invalid Date resets the stopwatch to 0', () => { + const { result } = renderHook(() => + useStopwatch() + ); + + act(() => { jest.advanceTimersByTime(5000); }); + + expect(result.current.seconds).toBe(5); + + act(() => result.current.reset(new Date('invalid'))); + + expect(result.current.seconds).toBe(0); + expect(result.current.totalMilliseconds).toBe(0); + }); }); }); \ No newline at end of file