Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
109 changes: 109 additions & 0 deletions cypress/component/useInterval.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
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 the interval 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.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(<TestComponent callbackFunction={callbackSpy} intervalValue={1000} autoStart={true} />);
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(<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");
});
});
83 changes: 83 additions & 0 deletions src/lib/hooks/useInterval.ts
Original file line number Diff line number Diff line change
@@ -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<number | null>(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 };
Loading