From a8bb256713f643fd918dfaa9f94775f53769dc5b Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Thu, 28 Aug 2025 11:58:01 +0200 Subject: [PATCH 01/13] Add usePolling hook --- cypress/component/usePolling.cy.tsx | 12 ++++ src/lib/hooks/usePolling.ts | 94 +++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 cypress/component/usePolling.cy.tsx create mode 100644 src/lib/hooks/usePolling.ts diff --git a/cypress/component/usePolling.cy.tsx b/cypress/component/usePolling.cy.tsx new file mode 100644 index 0000000..aed3685 --- /dev/null +++ b/cypress/component/usePolling.cy.tsx @@ -0,0 +1,12 @@ +import { usePolling } from "../../src/lib/hooks/usePolling"; + +function TestComponent() { + const result = usePolling({callback: () => {}, interval: 1000, stopped: false }); + return
Component using usePolling hook
; +} + +describe("usePolling Hook - Cypress Component Tests", () => { + it('component mounts', () => { + cy.mount() + }); +}); \ No newline at end of file diff --git a/src/lib/hooks/usePolling.ts b/src/lib/hooks/usePolling.ts new file mode 100644 index 0000000..3df5421 --- /dev/null +++ b/src/lib/hooks/usePolling.ts @@ -0,0 +1,94 @@ +import { useState, useRef, useCallback, useEffect } from "react"; + +/** + * The interface for the polling properties + * @param interval + */ +interface UsePollingProps { + /** + * The callback function + */ + callback: () => void | Promise; + /** + * The interval of the polling function + */ + interval: number; + /** + * + */ + stopped: boolean; +} + +/** + * The interface for the polling result + */ +interface UsePollingResult { + /** + * The current state of the hook wheter it's polling or not + */ + isPolling: boolean; + /** + * The function to start polling + */ + startPolling(): void; + /** + * The function to stopp polling + */ + stopPolling(): void; +} + +/** + * The polling hook + * @param props The parameters to set the function for the polling, the interval and if the hook is stopped or not + * @returns The functions startPolling, stoppPolling and a state wheter it's currently polling or not + */ +const usePolling = (props: UsePollingProps): UsePollingResult => { + const { callback, interval, stopped } = props; + + const [isPolling, setIsPolling] = useState(false); + + const persistedIsPolling = useRef(); + const timeout = useRef(); + + persistedIsPolling.current = isPolling; + + const stopPolling = useCallback(() => { + clearTimeout(timeout.current); + setIsPolling(false); + }, []); + + const runPolling = useCallback(() => { + timeout.current = setTimeout(() => { + void (async () => { + try { + await callback(); + } finally { + persistedIsPolling.current ? runPolling() : stopPolling(); + } + })(); + }, interval); + }, [callback, interval, stopPolling]); + + const startPolling = useCallback(() => { + setIsPolling(true); + runPolling(); + }, [runPolling]); + + useEffect(() => { + if (!stopped) { + startPolling(); + } + + return () => { + stopPolling(); + }; + }, [interval, stopped, startPolling, stopPolling]); + + return { + isPolling, + startPolling, + stopPolling, + }; +}; + +export { usePolling }; From 3fc199e9da19355e25c2ec492cce789dc0321308 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Thu, 28 Aug 2025 13:35:53 +0200 Subject: [PATCH 02/13] Add tests for usePolling --- cypress/component/usePolling.cy.tsx | 28 ++++++++++++++++++++++++++-- tsconfig.json | 5 +++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/cypress/component/usePolling.cy.tsx b/cypress/component/usePolling.cy.tsx index aed3685..097d43c 100644 --- a/cypress/component/usePolling.cy.tsx +++ b/cypress/component/usePolling.cy.tsx @@ -1,12 +1,36 @@ +import React from "react"; import { usePolling } from "../../src/lib/hooks/usePolling"; function TestComponent() { - const result = usePolling({callback: () => {}, interval: 1000, stopped: false }); - return
Component using usePolling hook
; + const result = usePolling({ callback: () => { console.log("Hello World"); }, interval: 1000, stopped: false }); + return (<> +
+ Component using usePolling hook + +
; + ) } describe("usePolling Hook - Cypress Component Tests", () => { it('component mounts', () => { + cy.window().then((win) => { + cy.spy(win.console, "log").as("consoleLog"); + }); + cy.mount() + + // Verify the component rendered + cy.get('[data-testid="test-component"]').should("be.visible"); + cy.get('[data-testid="test-component"]').should("contain.text", "Component using usePolling hook"); + + // Assert that console.log was called with "Hello World" + cy.get("@consoleLog").should("have.been.calledWith", "Hello World"); + cy.get("@consoleLog").should("have.been.calledOnce"); + }); + it('Start polling works', () => { + cy.clock(); + }) }); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 60cf2e3..9407b80 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,8 +15,9 @@ "noUnusedLocals": true, "noUnusedParameters": true, "allowSyntheticDefaultImports": true, - "baseUrl": "." + "baseUrl": ".", + "types": ["cypress"] }, - "include": ["src", "test"], + "include": ["src", "test", "cypress"], "exclude": ["node_modules", "dist"] } From ff74945505d61ddf1f97f0623cb958a720436369 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Thu, 28 Aug 2025 14:02:35 +0200 Subject: [PATCH 03/13] Add usepolling is stopped by default --- cypress/component/usePolling.cy.tsx | 39 ++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/cypress/component/usePolling.cy.tsx b/cypress/component/usePolling.cy.tsx index 097d43c..3933d9e 100644 --- a/cypress/component/usePolling.cy.tsx +++ b/cypress/component/usePolling.cy.tsx @@ -1,15 +1,20 @@ import React from "react"; import { usePolling } from "../../src/lib/hooks/usePolling"; -function TestComponent() { - const result = usePolling({ callback: () => { console.log("Hello World"); }, interval: 1000, stopped: false }); - return (<> +interface TestComponentProps{ + callback: () => void | Promise +} + +function TestComponent({ callback }: TestComponentProps) { + const result = usePolling({ callback: callback, interval: 1000, stopped: false }); + return ( + <>
Component using usePolling hook - -
; + + + ) } @@ -19,8 +24,12 @@ describe("usePolling Hook - Cypress Component Tests", () => { cy.spy(win.console, "log").as("consoleLog"); }); - cy.mount() + const callback = () => { + console.log("Hello World!"); + } + cy.mount() + // Verify the component rendered cy.get('[data-testid="test-component"]').should("be.visible"); cy.get('[data-testid="test-component"]').should("contain.text", "Component using usePolling hook"); @@ -30,7 +39,19 @@ describe("usePolling Hook - Cypress Component Tests", () => { cy.get("@consoleLog").should("have.been.calledOnce"); }); - it('Start polling works', () => { + it('Polling is stopped by default', () => { cy.clock(); - }) + + const callback = () => { + console.log("Hello World"); + } + + cy.spy(callback).as("callback"); + cy.mount() + cy.get("@callback").should("have.callCount", 0); + + }); + it('Start polling works', () => { + + }); }); \ No newline at end of file From b345ced763dfb42ac0b697eef2a95216f34e6819 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Thu, 28 Aug 2025 15:39:22 +0200 Subject: [PATCH 04/13] Add test --- cypress/component/usePolling.cy.tsx | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/cypress/component/usePolling.cy.tsx b/cypress/component/usePolling.cy.tsx index 3933d9e..8e41e57 100644 --- a/cypress/component/usePolling.cy.tsx +++ b/cypress/component/usePolling.cy.tsx @@ -11,7 +11,7 @@ function TestComponent({ callback }: TestComponentProps) { <>
Component using usePolling hook -

is polling? { result.isPolling}

+

is polling? { result.isPolling}

@@ -19,7 +19,7 @@ function TestComponent({ callback }: TestComponentProps) { } describe("usePolling Hook - Cypress Component Tests", () => { - it('component mounts', () => { + beforeEach(() => { cy.window().then((win) => { cy.spy(win.console, "log").as("consoleLog"); }); @@ -28,30 +28,31 @@ describe("usePolling Hook - Cypress Component Tests", () => { console.log("Hello World!"); } + cy.spy(callback).as("callback"); + cy.mount() + }); + + it('component mounts', () => { // Verify the component rendered cy.get('[data-testid="test-component"]').should("be.visible"); cy.get('[data-testid="test-component"]').should("contain.text", "Component using usePolling hook"); // Assert that console.log was called with "Hello World" - cy.get("@consoleLog").should("have.been.calledWith", "Hello World"); + cy.get("@consoleLog").should("have.been.calledWith", "Hello World!"); cy.get("@consoleLog").should("have.been.calledOnce"); }); - it('Polling is stopped by default', () => { - cy.clock(); - const callback = () => { - console.log("Hello World"); - } - - cy.spy(callback).as("callback"); - cy.mount() + it('Polling is stopped by default', () => { cy.get("@callback").should("have.callCount", 0); - }); - it('Start polling works', () => { + it('Start and stopp polling works', () => { + cy.contains('button', 'Start polling').click(); + cy.wait(3000); + cy.get("@callback").should("have.callCount", 3); }); + }); \ No newline at end of file From e60598e36286d012f40bc0b2c41bff1a05835dc7 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Thu, 28 Aug 2025 18:45:37 +0200 Subject: [PATCH 05/13] Add tests for usepolling hook --- cypress/component/usePolling.cy.tsx | 49 +++++++++++++++++++---------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/cypress/component/usePolling.cy.tsx b/cypress/component/usePolling.cy.tsx index 8e41e57..d120073 100644 --- a/cypress/component/usePolling.cy.tsx +++ b/cypress/component/usePolling.cy.tsx @@ -2,18 +2,18 @@ import React from "react"; import { usePolling } from "../../src/lib/hooks/usePolling"; interface TestComponentProps{ - callback: () => void | Promise + callbackFunction: () => void | Promise + intervalValue: number } -function TestComponent({ callback }: TestComponentProps) { - const result = usePolling({ callback: callback, interval: 1000, stopped: false }); +function TestComponent({ callbackFunction, intervalValue }: TestComponentProps) { + const result = usePolling({ callback: callbackFunction, interval: intervalValue, stopped: false }); return ( <>
Component using usePolling hook -

is polling? { result.isPolling}

- +
) } @@ -24,16 +24,14 @@ describe("usePolling Hook - Cypress Component Tests", () => { cy.spy(win.console, "log").as("consoleLog"); }); - const callback = () => { - console.log("Hello World!"); - } + const callbackSpy = cy.spy(() => { + console.log('Hello World!'); + }).as('callbackSpy'); - cy.spy(callback).as("callback"); - - cy.mount() + cy.mount() }); - it('component mounts', () => { + it('Component mounts', () => { // Verify the component rendered cy.get('[data-testid="test-component"]').should("be.visible"); @@ -45,14 +43,31 @@ describe("usePolling Hook - Cypress Component Tests", () => { }); - it('Polling is stopped by default', () => { - cy.get("@callback").should("have.callCount", 0); + it('Callback is not called on mount', () => { + cy.get('@callbackSpy').should('not.have.been.called'); + }); + + it('Start and stop polling works', () => { + cy.get('@callbackSpy').should('not.have.been.called'); + cy.contains('button', 'Start polling').click(); + cy.wait(1000); + cy.get('@callbackSpy').should('have.been.called'); + cy.contains('button', 'Stop polling').click(); }); - it('Start and stopp polling works', () => { + it('Setting an interval works', () => { + + const callbackSpy = cy.spy(() => { + console.log('Setting an interval works!'); + }).as('callbackSpy'); + + cy.mount() + + cy.get('@callbackSpy').should('not.have.been.called'); cy.contains('button', 'Start polling').click(); - cy.wait(3000); - cy.get("@callback").should("have.callCount", 3); + cy.wait(3100); + cy.get('@callbackSpy').should('have.been.called'); + cy.contains('button', 'Stop polling').click(); }); }); \ No newline at end of file From 0ae1edd8c93abba8a89b3ff41c823cc4f7baeef8 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Fri, 29 Aug 2025 09:23:05 +0200 Subject: [PATCH 06/13] Fixed tests --- cypress/component/usePolling.cy.tsx | 73 ++++++++++++++++------------- src/lib/hooks/usePolling.ts | 5 +- 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/cypress/component/usePolling.cy.tsx b/cypress/component/usePolling.cy.tsx index d120073..d103fd5 100644 --- a/cypress/component/usePolling.cy.tsx +++ b/cypress/component/usePolling.cy.tsx @@ -4,70 +4,79 @@ import { usePolling } from "../../src/lib/hooks/usePolling"; interface TestComponentProps{ callbackFunction: () => void | Promise intervalValue: number + isStopped: boolean } -function TestComponent({ callbackFunction, intervalValue }: TestComponentProps) { - const result = usePolling({ callback: callbackFunction, interval: intervalValue, stopped: false }); +function TestComponent({ callbackFunction, intervalValue, isStopped }: TestComponentProps) { + const result = usePolling({ callback: callbackFunction, interval: intervalValue, stopped: isStopped }); return ( <> -
- Component using usePolling hook - - -
- ) +
+ Component using usePolling hook + + +
+ ) } describe("usePolling Hook - Cypress Component Tests", () => { - beforeEach(() => { - cy.window().then((win) => { - cy.spy(win.console, "log").as("consoleLog"); - }); - + + it('Component mounts', () => { const callbackSpy = cy.spy(() => { console.log('Hello World!'); - }).as('callbackSpy'); + }).as('componentMountSpy'); - cy.mount() - }); - - it('Component mounts', () => { + cy.mount() - // Verify the component rendered cy.get('[data-testid="test-component"]').should("be.visible"); cy.get('[data-testid="test-component"]').should("contain.text", "Component using usePolling hook"); + }); - // Assert that console.log was called with "Hello World" - cy.get("@consoleLog").should("have.been.calledWith", "Hello World!"); - cy.get("@consoleLog").should("have.been.calledOnce"); + it('should not start to poll when isStopped is set on true', () => { + const callbackSpy = cy.spy(() => { + console.log('Does not start the polling automaticly'); + }).as('callbackSpy'); - }); + cy.mount() - it('Callback is not called on mount', () => { cy.get('@callbackSpy').should('not.have.been.called'); }); - it('Start and stop polling works', () => { + it('should remain stopped until start is called when isStopped is true', () => { + const callbackSpy = cy.spy(() => { + console.log('Does not start the polling automaticly'); + }).as('callbackSpy'); + + cy.mount() + cy.get('@callbackSpy').should('not.have.been.called'); cy.contains('button', 'Start polling').click(); cy.wait(1000); - cy.get('@callbackSpy').should('have.been.called'); cy.contains('button', 'Stop polling').click(); + cy.get('@callbackSpy').should('have.been.calledOnce'); }); - it('Setting an interval works', () => { + it('should start to poll automatically when stopped is false', () => { + const callbackSpy = cy.spy(() => { + console.log('The polling runs automatically when stopped is false.'); + }).as('callbackSpy'); + + cy.mount() + cy.get('@callbackSpy').should('have.been.called'); + }); + + it('should poll continuously until stopped when initially running', () => { const callbackSpy = cy.spy(() => { - console.log('Setting an interval works!'); + console.log('The polling runs automatically until it gets stopped.'); }).as('callbackSpy'); - cy.mount() + cy.mount() - cy.get('@callbackSpy').should('not.have.been.called'); - cy.contains('button', 'Start polling').click(); - cy.wait(3100); cy.get('@callbackSpy').should('have.been.called'); + cy.wait(1000); cy.contains('button', 'Stop polling').click(); + cy.get('@callbackSpy').should('have.been.calledTwice'); }); }); \ No newline at end of file diff --git a/src/lib/hooks/usePolling.ts b/src/lib/hooks/usePolling.ts index 3df5421..4984346 100644 --- a/src/lib/hooks/usePolling.ts +++ b/src/lib/hooks/usePolling.ts @@ -2,7 +2,6 @@ import { useState, useRef, useCallback, useEffect } from "react"; /** * The interface for the polling properties - * @param interval */ interface UsePollingProps { /** @@ -14,7 +13,7 @@ interface UsePollingProps { */ interval: number; /** - * + * The boolean to set if the hook is stopped or not */ stopped: boolean; } @@ -40,7 +39,7 @@ interface UsePollingResult { /** * The polling hook * @param props The parameters to set the function for the polling, the interval and if the hook is stopped or not - * @returns The functions startPolling, stoppPolling and a state wheter it's currently polling or not + * @returns The functions startPolling, stoppPolling and a boolean wheter the hook is currently polling or not */ const usePolling = (props: UsePollingProps): UsePollingResult => { const { callback, interval, stopped } = props; From 3ba1f8a9f0dd76e8c1929cae7064c19d2dd3d97a Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Fri, 29 Aug 2025 09:48:01 +0200 Subject: [PATCH 07/13] Add react utils hook usePolling --- CHANGELOG.md | 4 + cypress/component/usePolling.cy.tsx | 155 +++++++++++++++------------- src/lib/hooks/usePolling.ts | 2 +- tsconfig.json | 2 +- 4 files changed, 88 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11bddf3..bfcbf3b 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 + +- `usePolling` react utils hook diff --git a/cypress/component/usePolling.cy.tsx b/cypress/component/usePolling.cy.tsx index d103fd5..8c55776 100644 --- a/cypress/component/usePolling.cy.tsx +++ b/cypress/component/usePolling.cy.tsx @@ -1,82 +1,91 @@ import React from "react"; import { usePolling } from "../../src/lib/hooks/usePolling"; -interface TestComponentProps{ - callbackFunction: () => void | Promise - intervalValue: number - isStopped: boolean +interface TestComponentProps { + callbackFunction: () => void | Promise; + intervalValue: number; + isStopped: boolean; } function TestComponent({ callbackFunction, intervalValue, isStopped }: TestComponentProps) { - const result = usePolling({ callback: callbackFunction, interval: intervalValue, stopped: isStopped }); - return ( - <> -
- Component using usePolling hook - - -
- ) + const result = usePolling({ callback: callbackFunction, interval: intervalValue, stopped: isStopped }); + return ( + <> +
+ Component using usePolling hook + + +
+ + ); } describe("usePolling 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 usePolling hook"); - }); - - it('should not start to poll when isStopped is set on true', () => { - const callbackSpy = cy.spy(() => { - console.log('Does not start the polling automaticly'); - }).as('callbackSpy'); - - cy.mount() - - cy.get('@callbackSpy').should('not.have.been.called'); - }); - - it('should remain stopped until start is called when isStopped is true', () => { - const callbackSpy = cy.spy(() => { - console.log('Does not start the polling automaticly'); - }).as('callbackSpy'); - - cy.mount() - - cy.get('@callbackSpy').should('not.have.been.called'); - cy.contains('button', 'Start polling').click(); - cy.wait(1000); - cy.contains('button', 'Stop polling').click(); - cy.get('@callbackSpy').should('have.been.calledOnce'); - }); - - it('should start to poll automatically when stopped is false', () => { - const callbackSpy = cy.spy(() => { - console.log('The polling runs automatically when stopped is false.'); - }).as('callbackSpy'); - - cy.mount() - - cy.get('@callbackSpy').should('have.been.called'); - }); - - it('should poll continuously until stopped when initially running', () => { - const callbackSpy = cy.spy(() => { - console.log('The polling runs automatically until it gets stopped.'); - }).as('callbackSpy'); - - cy.mount() - - cy.get('@callbackSpy').should('have.been.called'); - cy.wait(1000); - cy.contains('button', 'Stop polling').click(); - cy.get('@callbackSpy').should('have.been.calledTwice'); - }); - -}); \ No newline at end of file + 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 usePolling hook"); + }); + + it("should not start to poll when isStopped is set on true", () => { + const callbackSpy = cy + .spy(() => { + console.log("Does not start the polling automaticly"); + }) + .as("callbackSpy"); + + cy.mount(); + + cy.get("@callbackSpy").should("not.have.been.called"); + }); + + it("should remain stopped until start is called when isStopped is true", () => { + const callbackSpy = cy + .spy(() => { + console.log("Does not start the polling automaticly"); + }) + .as("callbackSpy"); + + cy.mount(); + + cy.get("@callbackSpy").should("not.have.been.called"); + cy.contains("button", "Start polling").click(); + cy.wait(1000); + cy.contains("button", "Stop polling").click(); + cy.get("@callbackSpy").should("have.been.calledOnce"); + }); + + it("should start to poll automatically when stopped is false", () => { + const callbackSpy = cy + .spy(() => { + console.log("The polling runs automatically when stopped is false."); + }) + .as("callbackSpy"); + + cy.mount(); + + cy.get("@callbackSpy").should("have.been.called"); + }); + + it("should poll continuously until stopped when initially running", () => { + const callbackSpy = cy + .spy(() => { + console.log("The polling runs automatically until it gets stopped."); + }) + .as("callbackSpy"); + + cy.mount(); + + cy.get("@callbackSpy").should("have.been.called"); + cy.wait(1000); + cy.contains("button", "Stop polling").click(); + cy.get("@callbackSpy").should("have.been.calledTwice"); + }); +}); diff --git a/src/lib/hooks/usePolling.ts b/src/lib/hooks/usePolling.ts index 4984346..76d0814 100644 --- a/src/lib/hooks/usePolling.ts +++ b/src/lib/hooks/usePolling.ts @@ -47,7 +47,7 @@ const usePolling = (props: UsePollingProps): UsePollingResult => { const [isPolling, setIsPolling] = useState(false); const persistedIsPolling = useRef(); - const timeout = useRef(); + const timeout = useRef>(); persistedIsPolling.current = isPolling; diff --git a/tsconfig.json b/tsconfig.json index 9407b80..70b4387 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,7 @@ "noUnusedParameters": true, "allowSyntheticDefaultImports": true, "baseUrl": ".", - "types": ["cypress"] + "types": ["cypress", "node"] }, "include": ["src", "test", "cypress"], "exclude": ["node_modules", "dist"] From ae2af9c40b75fffa26faa8e1b5bff747619d8f6c Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Fri, 29 Aug 2025 11:32:24 +0200 Subject: [PATCH 08/13] Add react-utils useIntervall hook --- CHANGELOG.md | 2 +- cypress/component/useInterval.cy.tsx | 108 +++++++++++++++++++++++++++ cypress/component/usePolling.cy.tsx | 91 ---------------------- src/lib/hooks/useInterval.ts | 84 +++++++++++++++++++++ src/lib/hooks/usePolling.ts | 93 ----------------------- tsconfig.json | 4 +- 6 files changed, 195 insertions(+), 187 deletions(-) create mode 100644 cypress/component/useInterval.cy.tsx delete mode 100644 cypress/component/usePolling.cy.tsx create mode 100644 src/lib/hooks/useInterval.ts delete mode 100644 src/lib/hooks/usePolling.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index bfcbf3b..720ef85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,4 +9,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- `usePolling` react utils hook +- `useInterval` react utils hook diff --git a/cypress/component/useInterval.cy.tsx b/cypress/component/useInterval.cy.tsx new file mode 100644 index 0000000..34bee7d --- /dev/null +++ b/cypress/component/useInterval.cy.tsx @@ -0,0 +1,108 @@ +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 to poll 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.get("@callbackSpy").should("have.been.calledOnce"); + }); + + it("should start to poll 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 poll 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/cypress/component/usePolling.cy.tsx b/cypress/component/usePolling.cy.tsx deleted file mode 100644 index 8c55776..0000000 --- a/cypress/component/usePolling.cy.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React from "react"; -import { usePolling } from "../../src/lib/hooks/usePolling"; - -interface TestComponentProps { - callbackFunction: () => void | Promise; - intervalValue: number; - isStopped: boolean; -} - -function TestComponent({ callbackFunction, intervalValue, isStopped }: TestComponentProps) { - const result = usePolling({ callback: callbackFunction, interval: intervalValue, stopped: isStopped }); - return ( - <> -
- Component using usePolling hook - - -
- - ); -} - -describe("usePolling 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 usePolling hook"); - }); - - it("should not start to poll when isStopped is set on true", () => { - const callbackSpy = cy - .spy(() => { - console.log("Does not start the polling automaticly"); - }) - .as("callbackSpy"); - - cy.mount(); - - cy.get("@callbackSpy").should("not.have.been.called"); - }); - - it("should remain stopped until start is called when isStopped is true", () => { - const callbackSpy = cy - .spy(() => { - console.log("Does not start the polling automaticly"); - }) - .as("callbackSpy"); - - cy.mount(); - - cy.get("@callbackSpy").should("not.have.been.called"); - cy.contains("button", "Start polling").click(); - cy.wait(1000); - cy.contains("button", "Stop polling").click(); - cy.get("@callbackSpy").should("have.been.calledOnce"); - }); - - it("should start to poll automatically when stopped is false", () => { - const callbackSpy = cy - .spy(() => { - console.log("The polling runs automatically when stopped is false."); - }) - .as("callbackSpy"); - - cy.mount(); - - cy.get("@callbackSpy").should("have.been.called"); - }); - - it("should poll continuously until stopped when initially running", () => { - const callbackSpy = cy - .spy(() => { - console.log("The polling runs automatically until it gets stopped."); - }) - .as("callbackSpy"); - - cy.mount(); - - cy.get("@callbackSpy").should("have.been.called"); - cy.wait(1000); - cy.contains("button", "Stop polling").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..779cdd8 --- /dev/null +++ b/src/lib/hooks/useInterval.ts @@ -0,0 +1,84 @@ +import { useRef, useCallback, useState, useEffect } from "react"; + +/** + * The interface for the polling properties + */ +interface UseIntervalProps { + /** + * The callback function + */ + callback: () => void; + /** + * The interval of the polling function + */ + interval: number; + /** + * The boolean to set if the hook is initially polling or not + */ + autoStart: boolean; +} + +/** + * The interface for the polling result + */ +interface UseIntervalResult { + /** + * The current state of the hook wheter it's polling or not + */ + isRunning: boolean; + /** + * The function to start polling + */ + startInterval: () => void; + /** + * The function to stopp polling + */ + 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)) { + console.log("Starting interval"); + 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; + console.log("Starting interval", isRunning); + if (isRunning) { + startInterval(); + } + return stopInterval; + }, [callback, isRunning, interval, startInterval, stopInterval]); + + useEffect(() => { + if (autoStart) { + startInterval(); + } + }, [autoStart, startInterval]); + + return { isRunning, startInterval, stopInterval }; +}; + +export { useInterval }; diff --git a/src/lib/hooks/usePolling.ts b/src/lib/hooks/usePolling.ts deleted file mode 100644 index 76d0814..0000000 --- a/src/lib/hooks/usePolling.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { useState, useRef, useCallback, useEffect } from "react"; - -/** - * The interface for the polling properties - */ -interface UsePollingProps { - /** - * The callback function - */ - callback: () => void | Promise; - /** - * The interval of the polling function - */ - interval: number; - /** - * The boolean to set if the hook is stopped or not - */ - stopped: boolean; -} - -/** - * The interface for the polling result - */ -interface UsePollingResult { - /** - * The current state of the hook wheter it's polling or not - */ - isPolling: boolean; - /** - * The function to start polling - */ - startPolling(): void; - /** - * The function to stopp polling - */ - stopPolling(): void; -} - -/** - * The polling hook - * @param props The parameters to set the function for the polling, the interval and if the hook is stopped or not - * @returns The functions startPolling, stoppPolling and a boolean wheter the hook is currently polling or not - */ -const usePolling = (props: UsePollingProps): UsePollingResult => { - const { callback, interval, stopped } = props; - - const [isPolling, setIsPolling] = useState(false); - - const persistedIsPolling = useRef(); - const timeout = useRef>(); - - persistedIsPolling.current = isPolling; - - const stopPolling = useCallback(() => { - clearTimeout(timeout.current); - setIsPolling(false); - }, []); - - const runPolling = useCallback(() => { - timeout.current = setTimeout(() => { - void (async () => { - try { - await callback(); - } finally { - persistedIsPolling.current ? runPolling() : stopPolling(); - } - })(); - }, interval); - }, [callback, interval, stopPolling]); - - const startPolling = useCallback(() => { - setIsPolling(true); - runPolling(); - }, [runPolling]); - - useEffect(() => { - if (!stopped) { - startPolling(); - } - - return () => { - stopPolling(); - }; - }, [interval, stopped, startPolling, stopPolling]); - - return { - isPolling, - startPolling, - stopPolling, - }; -}; - -export { usePolling }; diff --git a/tsconfig.json b/tsconfig.json index 70b4387..6e97524 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,8 +16,8 @@ "noUnusedParameters": true, "allowSyntheticDefaultImports": true, "baseUrl": ".", - "types": ["cypress", "node"] + "types": ["node"] }, - "include": ["src", "test", "cypress"], + "include": ["src", "test"], "exclude": ["node_modules", "dist"] } From 8ef6d64d9e938a5c5234b790a561b7258fe8e142 Mon Sep 17 00:00:00 2001 From: "Sandro C." Date: Fri, 29 Aug 2025 11:52:16 +0200 Subject: [PATCH 09/13] Update src/lib/hooks/useInterval.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/lib/hooks/useInterval.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/hooks/useInterval.ts b/src/lib/hooks/useInterval.ts index 779cdd8..cee1cad 100644 --- a/src/lib/hooks/useInterval.ts +++ b/src/lib/hooks/useInterval.ts @@ -23,7 +23,7 @@ interface UseIntervalProps { */ interface UseIntervalResult { /** - * The current state of the hook wheter it's polling or not + * The current state of the hook whether it's polling or not */ isRunning: boolean; /** From 3ad77c19d96ed27273a5579140abc4a18cbbb05a Mon Sep 17 00:00:00 2001 From: "Sandro C." Date: Fri, 29 Aug 2025 11:52:25 +0200 Subject: [PATCH 10/13] Update src/lib/hooks/useInterval.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/lib/hooks/useInterval.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/hooks/useInterval.ts b/src/lib/hooks/useInterval.ts index cee1cad..8a9a5d9 100644 --- a/src/lib/hooks/useInterval.ts +++ b/src/lib/hooks/useInterval.ts @@ -31,7 +31,7 @@ interface UseIntervalResult { */ startInterval: () => void; /** - * The function to stopp polling + * The function to stop polling */ stopInterval(): void; } From c95e99c23fac4846c9d3f78e473461ba57c8d89e Mon Sep 17 00:00:00 2001 From: "Sandro C." Date: Fri, 29 Aug 2025 11:52:45 +0200 Subject: [PATCH 11/13] Update src/lib/hooks/useInterval.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/lib/hooks/useInterval.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/hooks/useInterval.ts b/src/lib/hooks/useInterval.ts index 8a9a5d9..da0d5d6 100644 --- a/src/lib/hooks/useInterval.ts +++ b/src/lib/hooks/useInterval.ts @@ -50,7 +50,6 @@ const useInterval = (props: UseIntervalProps): UseIntervalResult => { const startInterval = useCallback(() => { setIsRunning((prevIsRunning) => { if (!prevIsRunning && (!intervalRef.current || intervalRef.current === -1)) { - console.log("Starting interval"); intervalRef.current = window.setInterval(callbackRef.current, interval); } return true; From 4fa85f0353fc5efe1a9a450bc32749f70d1ea679 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Fri, 29 Aug 2025 13:02:19 +0200 Subject: [PATCH 12/13] Fix spelling --- cypress/component/useInterval.cy.tsx | 7 ++++--- src/lib/hooks/useInterval.ts | 21 ++++++++++----------- tsconfig.json | 3 +-- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/cypress/component/useInterval.cy.tsx b/cypress/component/useInterval.cy.tsx index 34bee7d..2c621b4 100644 --- a/cypress/component/useInterval.cy.tsx +++ b/cypress/component/useInterval.cy.tsx @@ -34,7 +34,7 @@ describe("useInterval Hook - Cypress Component Tests", () => { cy.get('[data-testid="test-component"]').should("contain.text", "Component using useInterval hook"); }); - it("should not start to poll when autostart is set on false", () => { + it("should not start the interval when autostart is set on false", () => { const callbackSpy = cy .spy(() => { console.log("Does not start the interval automaticly"); @@ -59,10 +59,11 @@ describe("useInterval Hook - Cypress Component Tests", () => { 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 to poll automatically when autostart is true", () => { + it("should start the interval automatically when autostart is true", () => { const callbackSpy = cy .spy(() => { console.log("The interval runs automatically when autostart is true."); @@ -74,7 +75,7 @@ describe("useInterval Hook - Cypress Component Tests", () => { cy.get("@callbackSpy").should("have.been.calledThrice"); }); - it("should poll continuously until stopped when autostart is true", () => { + 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."); diff --git a/src/lib/hooks/useInterval.ts b/src/lib/hooks/useInterval.ts index 779cdd8..05329df 100644 --- a/src/lib/hooks/useInterval.ts +++ b/src/lib/hooks/useInterval.ts @@ -1,7 +1,7 @@ import { useRef, useCallback, useState, useEffect } from "react"; /** - * The interface for the polling properties + * The interface for the properties of the useInterval hook */ interface UseIntervalProps { /** @@ -9,31 +9,32 @@ interface UseIntervalProps { */ callback: () => void; /** - * The interval of the polling function + * The interval in miliseconds for the interval function */ interval: number; /** - * The boolean to set if the hook is initially polling or not + * The boolean to set if the interval should start automatically or not + * @default false */ - autoStart: boolean; + autoStart?: boolean; } /** - * The interface for the polling result + * The interface for the result of the useInterval hook */ interface UseIntervalResult { /** - * The current state of the hook wheter it's polling or not + * The current state whether the interval is running or not */ isRunning: boolean; /** - * The function to start polling + * The function to start the interval */ startInterval: () => void; /** - * The function to stopp polling + * The function to stopp the interval */ - stopInterval(): void; + stopInterval: () => void; } /** @@ -50,7 +51,6 @@ const useInterval = (props: UseIntervalProps): UseIntervalResult => { const startInterval = useCallback(() => { setIsRunning((prevIsRunning) => { if (!prevIsRunning && (!intervalRef.current || intervalRef.current === -1)) { - console.log("Starting interval"); intervalRef.current = window.setInterval(callbackRef.current, interval); } return true; @@ -65,7 +65,6 @@ const useInterval = (props: UseIntervalProps): UseIntervalResult => { useEffect(() => { callbackRef.current = callback; - console.log("Starting interval", isRunning); if (isRunning) { startInterval(); } diff --git a/tsconfig.json b/tsconfig.json index 6e97524..60cf2e3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,8 +15,7 @@ "noUnusedLocals": true, "noUnusedParameters": true, "allowSyntheticDefaultImports": true, - "baseUrl": ".", - "types": ["node"] + "baseUrl": "." }, "include": ["src", "test"], "exclude": ["node_modules", "dist"] From ac3da6c2b08c1227c43df17e1aca7bbb54a15475 Mon Sep 17 00:00:00 2001 From: Dominic Baur Date: Fri, 29 Aug 2025 13:06:07 +0200 Subject: [PATCH 13/13] Fix --- src/lib/hooks/useInterval.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/hooks/useInterval.ts b/src/lib/hooks/useInterval.ts index 05329df..084fbd0 100644 --- a/src/lib/hooks/useInterval.ts +++ b/src/lib/hooks/useInterval.ts @@ -32,7 +32,7 @@ interface UseIntervalResult { */ startInterval: () => void; /** - * The function to stopp the interval + * The function to stop the interval */ stopInterval: () => void; }