Write useInterval(callback, delay) and export it from useInterval.js. It calls callback every delay milliseconds while the component is on the page. The tea shop uses it for a brewing timer that counts down.
It always calls the latest callback, the one from the most recent render, so the callback can read the current state and props. But a new callback must not restart the interval. Components pass a new arrow function on every render, and if each render restarted the interval, a component that renders often would keep pushing its next tick back.
A delay of null pauses it: nothing is called, and no timer is left waiting. Going back to a number starts it again. A new delay replaces the interval with one on the new timing. There's never more than one interval, and removing the component clears it.
function BrewTimer() {
const [seconds, setSeconds] = useState(180);
useInterval(() => setSeconds(seconds - 1), seconds > 0 ? 1000 : null);
return <p>{seconds} s left</p>;
}
The Preview shows Demo.jsx, a brewing timer that uses your hook.
const clock = useFakeTimers();
const tick = spy();
renderHook(() => useInterval(tick, 100));
await clock.advance(99);
expect(tick.callCount).toBe(0);
await clock.advance(251);
expect(tick.callCount).toBe(3);
const clock = useFakeTimers();
const first = spy();
const second = spy();
const { rerender } = renderHook(({ callback }) => useInterval(callback, 100), { initialProps: { callback: first } });
rerender({ callback: second });
await clock.advance(100);
expect(first.callCount).toBe(0);
expect(second.callCount).toBe(1);
const clock = useFakeTimers();
const first = spy();
const second = spy();
const { rerender } = renderHook(({ callback }) => useInterval(callback, 1000), { initialProps: { callback: first } });
await clock.advance(600);
rerender({ callback: second });
await clock.advance(400);
expect(second.callCount).toBe(1);
function BrewTimer() {
const [seconds, setSeconds] = React.useState(3);
useInterval(() => setSeconds(seconds - 1), seconds > 0 ? 1000 : null);
return <p>{seconds} s left</p>;
}
const clock = useFakeTimers();
render(<BrewTimer />);
await clock.advance(1000);
expect(screen.getByText("2 s left")).toBeInTheDocument();
await clock.advance(2000);
expect(screen.getByText("0 s left")).toBeInTheDocument();
await clock.advance(3000);
expect(screen.getByText("0 s left")).toBeInTheDocument();
expect(clock.pending()).toBe(0);
const clock = useFakeTimers();
const tick = spy();
renderHook(() => useInterval(tick, null));
expect(clock.pending()).toBe(0);
await clock.advance(5000);
expect(tick.callCount).toBe(0);
const clock = useFakeTimers();
const tick = spy();
const { rerender } = renderHook(({ delay }) => useInterval(tick, delay), { initialProps: { delay: 100 } });
await clock.advance(250);
expect(tick.callCount).toBe(2);
rerender({ delay: null });
await clock.advance(500);
expect(tick.callCount).toBe(2);
rerender({ delay: 100 });
await clock.advance(100);
expect(tick.callCount).toBe(3);
const clock = useFakeTimers();
const tick = spy();
const { rerender } = renderHook(({ delay }) => useInterval(tick, delay), { initialProps: { delay: 1000 } });
await clock.advance(500);
rerender({ delay: 200 });
await clock.advance(200);
expect(tick.callCount).toBe(1);
expect(clock.pending()).toBe(1);
await clock.advance(800);
expect(tick.callCount).toBe(5);
const clock = useFakeTimers();
const tick = spy();
const { rerender } = renderHook(() => useInterval(() => tick(), 100));
rerender();
rerender();
expect(clock.pending()).toBe(1);
await clock.advance(100);
expect(tick.callCount).toBe(1);
const clock = useFakeTimers();
const tick = spy();
const { unmount } = renderHook(() => useInterval(tick, 100));
await clock.advance(100);
unmount();
expect(clock.pending()).toBe(0);
await clock.advance(500);
expect(tick.callCount).toBe(1);
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with useInterval. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.