diff --git a/CHANGELOG.md b/CHANGELOG.md index 11bddf3..720ef85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,3 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +### Added + +- `useInterval` react utils hook diff --git a/cypress/component/useInterval.cy.tsx b/cypress/component/useInterval.cy.tsx new file mode 100644 index 0000000..2c621b4 --- /dev/null +++ b/cypress/component/useInterval.cy.tsx @@ -0,0 +1,109 @@ +import React from "react"; +import { useInterval } from "../../src/lib/hooks/useInterval"; + +interface TestComponentProps { + callbackFunction: () => void | Promise; + intervalValue: number; + autoStart: boolean; +} + +function TestComponent({ callbackFunction, intervalValue, autoStart }: TestComponentProps) { + const result = useInterval({ callback: callbackFunction, interval: intervalValue, autoStart: autoStart }); + return ( + <> +
+ Component using useInterval hook + + +
+ + ); +} + +describe("useInterval Hook - Cypress Component Tests", () => { + it("Component mounts", () => { + const callbackSpy = cy + .spy(() => { + console.log("Hello World!"); + }) + .as("componentMountSpy"); + + cy.mount(); + + cy.get('[data-testid="test-component"]').should("be.visible"); + cy.get('[data-testid="test-component"]').should("contain.text", "Component using useInterval hook"); + }); + + it("should not start the interval when autostart is set on false", () => { + const callbackSpy = cy + .spy(() => { + console.log("Does not start the interval automaticly"); + }) + .as("callbackSpy"); + + cy.mount(); + cy.wait(3000); + cy.get("@callbackSpy").should("not.have.been.called"); + }); + + it("should remain stopped until start is called when autostart is false", () => { + const callbackSpy = cy + .spy(() => { + console.log("Does not start the interval automaticly"); + }) + .as("callbackSpy"); + + cy.mount(); + cy.wait(3000); + cy.get("@callbackSpy").should("not.have.been.called"); + cy.contains("button", "Start interval").click(); + cy.wait(1000); + cy.contains("button", "Stop interval").click(); + cy.wait(3000); + cy.get("@callbackSpy").should("have.been.calledOnce"); + }); + + it("should start the interval automatically when autostart is true", () => { + const callbackSpy = cy + .spy(() => { + console.log("The interval runs automatically when autostart is true."); + }) + .as("callbackSpy"); + + cy.mount(); + cy.wait(3000); + cy.get("@callbackSpy").should("have.been.calledThrice"); + }); + + it("should be running continuously until stopped when autostart is true", () => { + const callbackSpy = cy + .spy(() => { + console.log("The interval runs automatically until it gets stopped."); + }) + .as("callbackSpy"); + + cy.mount(); + cy.wait(1000); + cy.get("@callbackSpy").should("have.been.calledOnce"); + cy.contains("button", "Stop interval").click(); + cy.get("@callbackSpy").should("have.been.calledOnce"); + }); + + it("should only be possible to start the interval once", () => { + const callbackSpy = cy + .spy(() => { + console.log("The interval can only be started once."); + }) + .as("callbackSpy"); + + cy.mount(); + + cy.contains("button", "Start interval").click(); + cy.contains("button", "Start interval").click(); + + cy.get("@callbackSpy").should("have.been.calledOnce"); + cy.wait(1000); + cy.contains("button", "Stop interval").click(); + cy.get("@callbackSpy").should("have.been.calledTwice"); + }); +}); diff --git a/src/lib/hooks/useInterval.ts b/src/lib/hooks/useInterval.ts new file mode 100644 index 0000000..084fbd0 --- /dev/null +++ b/src/lib/hooks/useInterval.ts @@ -0,0 +1,83 @@ +import { useRef, useCallback, useState, useEffect } from "react"; + +/** + * The interface for the properties of the useInterval hook + */ +interface UseIntervalProps { + /** + * The callback function + */ + callback: () => void; + /** + * The interval in miliseconds for the interval function + */ + interval: number; + /** + * The boolean to set if the interval should start automatically or not + * @default false + */ + autoStart?: boolean; +} + +/** + * The interface for the result of the useInterval hook + */ +interface UseIntervalResult { + /** + * The current state whether the interval is running or not + */ + isRunning: boolean; + /** + * The function to start the interval + */ + startInterval: () => void; + /** + * The function to stop the interval + */ + stopInterval: () => void; +} + +/** + * The useInterval hook + * @param props The props for the useInterval hook, see {@link UseIntervalProps} + * @returns The result of the useInterval, see {@link UseIntervalResult} + */ +const useInterval = (props: UseIntervalProps): UseIntervalResult => { + const { autoStart, callback, interval } = props; + const [isRunning, setIsRunning] = useState(false); + const intervalRef = useRef(null); + const callbackRef = useRef<() => void>(callback); + + const startInterval = useCallback(() => { + setIsRunning((prevIsRunning) => { + if (!prevIsRunning && (!intervalRef.current || intervalRef.current === -1)) { + intervalRef.current = window.setInterval(callbackRef.current, interval); + } + return true; + }); + }, [interval]); + + const stopInterval = useCallback(() => { + setIsRunning(false); + window.clearInterval(intervalRef.current || -1); + intervalRef.current = -1; + }, []); + + useEffect(() => { + callbackRef.current = callback; + if (isRunning) { + startInterval(); + } + return stopInterval; + }, [callback, isRunning, interval, startInterval, stopInterval]); + + useEffect(() => { + if (autoStart) { + startInterval(); + } + }, [autoStart, startInterval]); + + return { isRunning, startInterval, stopInterval }; +}; + +export { useInterval };