Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
108 changes: 108 additions & 0 deletions cypress/component/useInterval.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React from "react";
import { useInterval } from "../../src/lib/hooks/useInterval";

interface TestComponentProps {
callbackFunction: () => void | Promise<void>;
intervalValue: number;
autoStart: boolean;
}

function TestComponent({ callbackFunction, intervalValue, autoStart }: TestComponentProps) {
const result = useInterval({ callback: callbackFunction, interval: intervalValue, autoStart: autoStart });
return (
<>
<div data-testid="test-component">
Component using useInterval hook
<button onClick={result.startInterval}>Start interval</button>
<button onClick={result.stopInterval}>Stop interval</button>
</div>
</>
);
}

describe("useInterval Hook - Cypress Component Tests", () => {
it("Component mounts", () => {
const callbackSpy = cy
.spy(() => {
console.log("Hello World!");
})
.as("componentMountSpy");

cy.mount(<TestComponent callbackFunction={callbackSpy} intervalValue={1000} autoStart={false} />);

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(<TestComponent callbackFunction={callbackSpy} intervalValue={1000} autoStart={false} />);
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(<TestComponent callbackFunction={callbackSpy} intervalValue={1000} autoStart={false} />);
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");
Comment thread
dom-baur marked this conversation as resolved.
});

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(<TestComponent callbackFunction={callbackSpy} intervalValue={1000} autoStart={true} />);
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(<TestComponent callbackFunction={callbackSpy} intervalValue={1000} autoStart={true} />);
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(<TestComponent callbackFunction={callbackSpy} intervalValue={1000} autoStart={false} />);

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");
});
});
84 changes: 84 additions & 0 deletions src/lib/hooks/useInterval.ts
Original file line number Diff line number Diff line change
@@ -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
Comment thread
dom-baur marked this conversation as resolved.
Outdated
*/
interval: number;
/**
* The boolean to set if the hook is initially polling or not
Comment thread
dom-baur marked this conversation as resolved.
Outdated
*/
autoStart: boolean;
}

/**
* The interface for the polling result
*/
interface UseIntervalResult {
/**
* The current state of the hook wheter it's polling or not
Comment thread
neoscie marked this conversation as resolved.
Outdated
*/
isRunning: boolean;
/**
* The function to start polling
*/
startInterval: () => void;
/**
* The function to stopp polling
Comment thread
neoscie marked this conversation as resolved.
Outdated
*/
stopInterval(): void;
Comment thread
dom-baur marked this conversation as resolved.
Outdated
}

/**
* 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<number | null>(null);
const callbackRef = useRef<() => void>(callback);

const startInterval = useCallback(() => {
setIsRunning((prevIsRunning) => {
if (!prevIsRunning && (!intervalRef.current || intervalRef.current === -1)) {
console.log("Starting interval");
Comment thread
neoscie marked this conversation as resolved.
Outdated
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);
Comment thread
dom-baur marked this conversation as resolved.
Outdated
if (isRunning) {
startInterval();
}
return stopInterval;
}, [callback, isRunning, interval, startInterval, stopInterval]);

Copy link

Copilot AI Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This useEffect could cause infinite re-renders. The cleanup function stopInterval is returned but the effect depends on stopInterval itself, and calling startInterval inside the effect when isRunning is true could trigger the effect again. Consider restructuring to avoid this dependency cycle.

Suggested change
console.log("Starting interval", isRunning);
if (isRunning) {
startInterval();
}
return stopInterval;
}, [callback, isRunning, interval, startInterval, stopInterval]);
if (isRunning) {
if (!intervalRef.current || intervalRef.current === -1) {
intervalRef.current = window.setInterval(() => {
callbackRef.current();
}, interval);
}
} else {
if (intervalRef.current && intervalRef.current !== -1) {
window.clearInterval(intervalRef.current);
intervalRef.current = -1;
}
}
return () => {
if (intervalRef.current && intervalRef.current !== -1) {
window.clearInterval(intervalRef.current);
intervalRef.current = -1;
}
};
}, [callback, isRunning, interval]);

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@neotrow is this accurate?

useEffect(() => {
if (autoStart) {
startInterval();
}
}, [autoStart, startInterval]);

return { isRunning, startInterval, stopInterval };
};

export { useInterval };
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"noUnusedLocals": true,
"noUnusedParameters": true,
"allowSyntheticDefaultImports": true,
"baseUrl": "."
"baseUrl": ".",
"types": ["node"]
Comment thread
dom-baur marked this conversation as resolved.
Outdated
},
"include": ["src", "test"],
"exclude": ["node_modules", "dist"]
Expand Down
Loading