My goal is to test simple component that count down from 60s to 0 and then run a function.
Comonent:
import { useEffect, useState } from "react";
export default function Clock({ dispatch }) {
const [clock, setClock] = useState(60);
useEffect(() => {
const tick = setInterval(() => {
setClock(clock - 1);
}, 1000);
if (clock === 0) {
dispatch({ type: "breakDone" });
}
return function cleanUp() {
clearInterval(tick);
};
});
console.log(clock);
return (
<div className="clock__container">
<h1>{clock}s</h1>
<p data-testid="clock-paragraph">
lorem ipsum
</p>
</div>
);
}
I have a problem with testing component. I have tried to test it with different methods, however my timer have never go down to 59s.
What I have tired:
Gives the worst feedback -> fake positive
test("Value of clock after 1s", async () => {
render(<Clock />);
expect(screen.getByText("60s")).toBeInTheDocument();
setTimeout(() => {
expect(screen.getByText("59s")).toBeInTheDocument();
}, 1000);
});
test("Value of clock after 1s", async () => {
render(<Clock />);
expect(screen.getByText("60s")).toBeInTheDocument();
jest.advanceTimersByTime(10000);
expect(await screen.findByText("59s")).toBeInTheDocument();
});
This solution correctly test 1s warrant, however fails with 10s.
test("Value of clock after 1s", async () => {
render(<Clock />);
const headerElement = screen.getByRole("heading");
jest.advanceTimersByTime(1000);
await waitFor(() => expect(headerElement.innerHTML).toBe("59s"));
});
I have no clue what the problem is to be honest.
Your useEffect hook needs a dependency array for the clock state
like so:
useEffect(() => {
const tick = setInterval(() => {
setClock(clock - 1);
}, 1000);
if (clock === 0) {
dispatch({ type: "breakDone" });
}
return function cleanUp() {
clearInterval(tick);
};
},[clock]);
Related
I want to JEST test a React Component which updates state after a timeout both before and after rendering state. The expect() assertion works fine in a test() but breaks when used inside a SetTimeout callback. An error states that expect is not defined within the setTimeout callback.
I have created a sandbox here:
https://codesandbox.io/s/jest-delayed-render-test-j5b7l
Simple component update state after SetTimeout.
// delayed.js
import { useEffect, useState } from "react";
export const Delayed = () => {
let [value, setValue] = useState("foo");
useEffect(() => {
let handle = setTimeout(() => {
setValue("bar");
}, 3000);
return () => {
clearTimeout(handle);
};
}, []);
return (
<>
<h1>MyPromise</h1>
<p>value = {value}</p>
</>
);
};
Simple test asserts the component before and update timeout
// delayed.test.js
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import { Delayed } from "./delayed";
let container = null;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
unmountComponentAtNode(container);
container.remove();
container = null;
});
it("checks initial value", async () => {
await act(async () => {
render(<Delayed />, container);
});
expect(container.textContent).toContain("foo");
});
it("checks final value", async () => {
await act(async () => {
render(<Delayed />, container);
});
setTimeout(() => {
// this doesnt get executed!
// error: expect is not defined
expect(container.textContent).toContain("1234");
}, 2000);
});
this is easily fixed passing in done as a param on the test.
it("checks final value", async (done) => {
await act(async () => {
render(<Delayed />, container);
});
setTimeout(() => {
expect(container.textContent).toContain("bar");
done();
}, 3000);
});
as described here:
https://www.pluralsight.com/guides/test-asynchronous-code-jest
Trying to test Custom useInterval Hook but jest.advanceTimersByTime(199); and jest.advanceTimersToNextTimer(1); don't seem to be working.
I log jest.getTimerCount() anywhere and it returns 0;
Custom Hook:
import { useRef, useEffect } from 'react';
function useInterval(callback: () => void, delay: number | null) {
const savedCallback = useRef<() => void | null>();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
});
// Set up the interval.
useEffect(() => {
function tick() {
console.log("here"); // This never gets logged !!!!
if (typeof savedCallback?.current !== 'undefined') {
console.log(delay, savedCallback);
}
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
export default useInterval;
Test:
import useInterval from "./useInterval";
import { renderHook } from '#testing-library/react-hooks';
describe("useInterval Hook:", () => {
let callback = jest.fn();
beforeAll(() => {
// we're using fake timers because we don't want to
// wait a full second for this test to run.
jest.useFakeTimers();
});
afterEach(() => {
callback.mockRestore();
jest.clearAllTimers();
});
afterAll(() => {
jest.useRealTimers();
});
test('should init hook with delay', () => {
const { result } = renderHook(() => useInterval(callback, 5000));
expect(result.current).toBeUndefined();
expect(setInterval).toHaveBeenCalledTimes(1);
expect(setInterval).toHaveBeenCalledWith(expect.any(Function), 5000);
});
test('should repeatedly calls provided callback with a fixed time delay between each call', () => {
const { result } = renderHook(() => useInterval(callback, 200));
expect(callback).not.toHaveBeenCalled();
// fast-forward time until 1s before it should be executed
jest.advanceTimersByTime(199);
expect(callback).not.toHaveBeenCalled(); // FAILS
// jest.getTimerCount() here returns 0
// fast-forward until 1st call should be executed
jest.advanceTimersToNextTimer(1);
expect(callback).toHaveBeenCalledTimes(1);
// fast-forward until next timer should be executed
jest.advanceTimersToNextTimer();
expect(callback).toHaveBeenCalledTimes(2);
// fast-forward until 3 more timers should be executed
jest.advanceTimersToNextTimer(3);
expect(callback).toHaveBeenCalledTimes(5);
});
});
I solved it by moving jest.useFakeTimers(); to the beforeEach block instead of beforeAll.
I struggled with this for hours until finally I found this article. https://overreacted.io/making-setinterval-declarative-with-react-hooks/
Just copy the useInterval function verbatim, and then use it with the syntax also provided. It just works correctly with no fuss.
I have an application required to run API calls every 3 seconds. I used useInterval to call API, every 3 seconds I received the API result. When I update from redux, something went wrong with useInterval.
UseInterval
export default function useInterval(callback, delay, immediate = true) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
(async() => {
async function tick() {
await savedCallback.current();
}
if (delay !== null) {
if (immediate) {
await tick();
}
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
})();
}, [delay]);
}
Main
const enhance = connect(
(state, ownProps) => ({
modal: state.modal[ownProps.id]
}),
{ updateFromRedux }
);
const container = ({ id, modal, updateFromRedux }) => {
useInterval(() => {
# -----> This scope of codes went wrong when updateFromRedux is called <-----
let modalId = modal["id"]
return fetch(`https://api-url.com/${modalId}`)
.then(res => res.json())
.then(
(result) => {
updateFromRedux(id, result)
}
)
}, 3000)
})
export default enhance(container);
Redux
export const updateFromRedux = (id, details) => ({
type: UPDATE_DETAILS,
payload: { id, details }
});
Problem
The modalId produces an inconsistent output such as undefined inside useInterval after updateFromRedux redux method is called.
In a ReactJs app, setInterval is being used in the demo below. Currently, it waits for 10 seconds even at inital load, instead, it should call immediately then after 10 second it should call again. How can i achieve this?
const callApiAfterOneSec = async () => {
await axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`)
.then(res => {
setLoading(false);
setPost(res.data);
})
.catch(err => {
setLoading(false);
console.log(err);
});
};
useEffect(() => {
const interval = setInterval(callApiAfterOneSec, 10000);
return () => {
clearInterval(interval);
};
}, [post]);
Try to add another useEffect(), fired only once:
useEffect(() => {
callApiAfterOneSec();
}, []);
You could use this as class component
class App extends React.Component {
interval;
constructor() {
super()
this.state = {
data: {}
}
}
componentDidMount() {
this.loadData();
this.interval = setInterval(this.loadData, 5000);
}
componentWillUnmount() {
clearTimeout(this.interval);
}
async loadData() {
try {
const res = await fetch('https://jsonplaceholder.typicode.com/posts/1');
await res.json().then(res => {
this.setState({
data: res
})
})
} catch (e) {
console.log(e);
}
}
render() {
return (
<div>
<ul>
<li>UserId : {this.state.data.userId}</li>
<li>Title : {this.state.data.title}</li>
<li>Body : {this.state.data.body}</li>
</ul>
</div>
);
}
}
render(<App />, document.getElementById('root'));
As #xadm mention you can useEffect two times as per requirement simple fix
useEffect(() => {
callApiAfterOneSec();
}, []);
This is just a hack that might solve you problem.
const [rendered, setRendered] = useState(false);
useEffect(() => {
let interval = null;
if(rendered) interval = setInterval(callApiAfterOneSec, 10000)
setRendered(true);
return () => {
clearInterval(interval)
}
}, [post, rendered])
I have the following react class component to call an API every 10 seconds. Its works with no issues.
class Alerts extends Component {
constructor() {
this.state = {
alerts: {},
}
}
componentDidMount() {
this.getAlerts()
this.timerId = setInterval(() => this.getAlerts(), 10000)
}
componentWillUnmount() {
clearInterval(this.timerId)
}
getAlerts() {
fetch(this.getEndpoint('api/alerts/all"))
.then(result => result.json())
.then(result => this.setState({ alerts: result }))
}
render() {
return (
<>
<ListAlerts alerts={this.state.alerts} />
</>
)
}
}
I am trying covert this to a react functional component. This is my attempt so far.
const Alerts = () => {
const [alerts, setAlerts] = useState([])
useEffect(() => {
getAlerts()
setInterval(() => getAlerts(), 10000)
}, [])
getAlerts() {
fetch(this.getEndpoint('api/alerts/all"))
.then(result => result.json())
.then(result => setAlerts(result)
}
return (
<>
<ListAlerts alerts={alerts} />
</>
)
}
Please can someone help me complete the example? Is useEffect the correct usage or is there a better option?
Any help would be appreciated
One issue here is that this.getEndpoint will not work from a function component. It seems the original Alerts class component is missing some code since that must be implemented somewhere.
Another issue is that the interval is not being cleaned up - you should return a cleanup function from the effect body to clear the timer.
Lastly there's no reason to re-define getAlerts on every render, defining it once inside of the effect body would be better.
After cleaning up some missing parens, etc. my final implementation would look something like:
function getEndpoint(path) {
return ...; // finish implementing this
}
const Alerts = () => {
const [alerts, setAlerts] = useState([])
useEffect(() => {
function getAlerts() {
fetch(getEndpoint('api/alerts/all'))
.then(result => result.json())
.then(result => setAlerts(result))
}
getAlerts()
const interval = setInterval(() => getAlerts(), 10000)
return () => {
clearInterval(interval);
}
}, [])
return (
<>
<ListAlerts alerts={alerts} />
</>
)
}
I found this blog by Dan Abramov which explains the idea of a useInterval hook that solves this problem.
You can use it like this :
function Counter() {
useInterval(() => {
callMyApi()
}, 1000);
}
And declare the useInterval hook this way :
import React, { useState, useEffect, useRef } from 'react';
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
Hope it helps someone!