I have a simple react functional component.
The code should be self-explanatory. If the status is equal to 'some status', go to a URL, fetch some information, and set the state of the component to the information that has been fetched. On the return () just display the information. Everything works fine, the id of the data is displayed. However, when I open the dev tools and do the inspection, the console.log("data"+data.id); is run indefinitely. I wonder what is making it run indefinitely.
if I remove the change data(data) from inside the fetch, the console.log does not run indefinitely.
I am scratching my head as to why, changing the status would make the code enter in an infinite loop?
function ReturnInfo(props) {
var currentstatus = props.currentstatus; // receiving the current status as prop from parent.
const [data, changeData] = useState({});
let { slug } = useParams(); // getting the slug.
if (currentstatus == 'some status') {
fetch(`https:someurl/${slug}`).
then(res => res.json()).
then(data => {
console.log("data" + data.id);
changeData(data);
});
return (
<div>
<div>
{data.id}
</div>
</div>
)
}
else {
return (
<p>try harder!</p>
)
}
}
You should use useEffect, The Effect Hook lets you perform side effects in function components:
useEffect docs = https://reactjs.org/docs/hooks-effect.html
And if you don't add the dependency array it will run on each update.
Simple wrap your side-effect/async task code within the useEffect function.
And add the dependency array. add empty array if you want to run it only once.
useEffect(() => {
fetch(`https:someurl/${slug}`).
then(res => res.json()).
then(data => {
console.log("data" + data.id);
changeData(data);
});
}, [slug])
It will stop the unnecessary rerender.
Edit Try this
function ReturnInfo(props) {
var currentstatus = props.currentstatus; // receiving the current status as prop from parent.
const [data, changeData] = useState({});
let { slug } = useParams(); // getting the slug.
useEffect(() => {
fetch(`https:someurl/${slug}`).
then(res => res.json()).
then(data => {
console.log("data" + data.id);
changeData(data);
});
}, [slug])
if (currentstatus === 'some status') {
return (
<div>
<div>
{data.id}
</div>
</div>
)
}
return <p>try harder!</p>
}
You are calling the function every time the component is rendered. The function gets called, it updates the state, and makes the component to re-render.
You should call the function when an event occurrs or change the currentstatus value every time the block is executed.
changeData(data) will cause reevaluation of the component. Which leads to call fetch again, which make infinite loop.
useEffect(() {
if (currentstatus == 'some status') {
fetch(`https:someurl/${slug}`).
then(res => res.json()).
then(data => {
console.log("data" + data.id);
changeData(data);
});
}},[])
Related
I feel like I am missing something when it comes to testing React components with async fetch operations.
I have a following component like this...
export default function Page() {
const [result, setResult] = useState();
async function initialize() {
const response = api.fetchData();
setResult(response);
}
useEffect(() => {
initialize();
}, []);
return (isLoading ? (
<div>Fetching Data...</div>
) : (
<div className="page-result-name">{result.name}</div>
)
}
I want to create the following test.
test('Page rendering test.', async () => {
jest.spyOn(api, 'fetchData').mockResolvedValue({ name: 'User 1'});
const pageRendering = renderer.create(<Page />);
// Wait? How to wait?
// I have tried the following... as well as other method.
// await waitFor(() => {
// const element = document.getElementByClassName('page-result-name')[0] as
// HTMLElement;
// expect(element.innerHTML.includes('User 1')).toBeTruthy();
// });
expect(pageRendering.toJSON()).toMatchSnapshot();
});
The issue I am running into is that the data has not been returned by the time the snapshot is taken.
Regardless of the approach I run into issues with warnings like the following
Warning: It looks like you're using the wrong act() around your test interactions
Or it always displays Fetching Data...
I have used render from #testing-library/react and the waitFor and things work well. I don't know how to generate a snapshot for that.
Any help here would be appreciated!!
Cheers!!
For async operations the tricky is waiting for loading component to be removed from the screen, this way you assure that the your component is completed loaded and then you can expect the snapshot:
test('Page rendering test.', async () => {
jest.spyOn(api, 'fetchData').mockResolvedValue({ name: 'User 1'});
const pageRendering = renderer.create(<Page />);
// Wait? How to wait?
// wait for your data to be loaded before you make your assertion.
await waitForElementToBeRemoved(screen.getByText('Fetching Data...'));
expect(pageRendering.toJSON()).toMatchSnapshot();
});
I'm trying to test a component that loads data asynchronously when mounted. The component works as expected, it's just the test that's giving me issues. The component's async loadData() function hangs at await axios.get() while jest test runner is in the component.vm.$nextTick(). As a result, the checks in the $nextTick loop never pass.
Immediately after the $nextTick loop times out, the component's await statement completes and the component renders itself. axios is mocked, so it should resolve really fast. If I remove the await and just fill in a constant instead, the entire thing executes as expected.
I'm guessing that $nextTick loop is not asynchronous and it's consuming the thread, even though this is the recommended way of testing asynchronous stuff. The problem is, I don't have an onclick async handler to await: this method is called from onMount.
Unfortunately, I don't know how to make a jsFiddle of this one, so I hope this will be enough:
my component (the relevant parts)
export default {
data() { return { content: '' }; },
mounted() { this.loadDoc() }
methods: {
async loadDoc() {
const res = await axios.get('some url'); // <-- this is the line that hangs until timeout
// const res = { data: 'test data'}; // this would test just fine
this.content = res.data;
}
}
}
and my component.spec.js:
jest.mock('axios', () => ({
get: async (url) => {
return { data: 'test data' };
}
};
describe('my super test', () => {
it('renders', (done) => {
const doc = shallowMount(myComponent);
doc.vm.$nextTick(() => {
expect(doc.html()).toContain('test data'); // <-- this never matches
done();
});
});
});
I would delete, but I just spent quite some hours for something that was suggested in the docs, but not explained that it's the only way... I'm hoping somebody else finds this useful.
Using flush-promises package instead of $nextTick loop immediately "fixed" the problem
Code sample (rework of above):
describe('my super test', () => {
it('renders', async() => {
const doc = shallowMount(myComponent);
await flushPromises();
expect(doc.html()).toContain('test data'); // <-- now it works
});
});
I need to call the query on status change when component mounted.
Let's say I have 5 status in array and I'm looping that array.
The expected output should be: that query must be called 5 times.
But the problem is only one time query was called for the status which is at last position in an array.
const loadedStatus = ["A", "B", "C", "D", "E"]
const [listSomeData] = useLazyQuery(LIST_SOME_DATA, {
fetchPolicy: 'network-only'
});
const listSomeDataHandler = async (statusId, pagination = {}, filters = {}) => {
await listSomeData({
variables: {
statusId,
page: pagination?.page,
pageSize: pagination?.pageSize,
...filters
}
});
};
useEffect(async () => {
loadedStatus &&
Promise.all(
loadedStatus.map(status => {
listSomeDataHandler(
status.id,
{ page: 0, pageSize: 10 },
filters
);
})
);
},[loadedStatus]);
async in useEffect should be considered an antipattern (here an explanation) and you should get a warning like Warning: An Effect function must not return anything besides a function, which is used for clean-up..
Not only but your useEffect has a missing dependency: listSomeDataHandler.
In this case, much better call an async function inside the useEffect (and move listSomeDataHandler inside useEffect). So your code becomes:
useEffect(() => {
const listSomeDataHandler = async (status) => {
await listSomeData(status);
};
(async () => {
Promise.all(
loadedStatus.map((status) => {
return listSomeDataHandler(status);
})
);
})();
}, [loadedStatus]);
Here I made a codesandbox example and, as you can see, listSomeDataHandler will be called 5 times.
I am close to displaying my axios response results to a table but i keep getting the error. Still learning React. See the error below
Below is the code making API call
const listJobs = () =>{
axios.get( `${process.env.REACT_APP_API}/search-projects`,
{params: {category
}
})
.then(response => {
console.log('LOG SUCCESS --', response.data);
const results = response.data;
setValues({...values, results: results});
console.log('My State Data', results);
})
}
This is to display the data on the table
const { results } = values;
{ results.map(({id, title, description, budget}, index ) => {
return (
<tr key={id}>
<td>{id}</td>
<td>{title}</td>
<td>{description}</td>
<td>{budget}</td>
</tr>
);
})}
Axios makes an asynchronous request: you need to call it from useEffect hook (or componentDidMount) like so:
useEffect(() => {
axios.get(`${process.env.REACT_APP_API}/search-projects`).then(res => console.log(res))
})
Other problem is you aren't checking if the request finished and the state is updated
{
results &&
results.map(({ id, title, description, budget }, index) => {
return (
<tr key={id}>
<td>{id}</td>
<td>{title}</td>
<td>{description}</td>
<td>{budget}</td>
</tr>
)
})
}
And finally, the request must be an array to use Array.prototype.map() function.
My guess is that the issue here is that, until your async axios call returns, results will be undefined, and so on the first few attempts to render your component, it is trying to call .map() on undefinded which won't work.
You could add some kind of guard in your jsx like:
{ results && results.map(...) }
Or initialize your results to an empty array at first, and reassign to the results of your axios call once that resolves.
const results = [];
if( Array.isArray( response.data ) ){
setValues({...values, results: results});
};
I'm seeing the error below with the second setState for the group.
Invariant Violation: React.Children.only expected to receive a single React element child
From reading it looks like I need to wrap these statements inside a root component or something similar; anyone help?
async componentDidMount() {
if (!this.props.isAuthenticated) {
return;
}
try {
const notes = await this.notes();
this.setState({ notes });
const group = await this.group();
this.setState({ group });
} catch (e) {
alert(e);
}
The full file can be found here - github.com/dodo83/giftlist/blob/master/Home.js
you need to return null, <></> or some kind of React component, e.g. <div>Foo</div>
Need to see your render function
I would also suggest you group the setState calls to a single call, e.g.
this.setState({ notes, group}); once the promises have resolved
Personally, i'd simplify your code to something more along these lines:
async componentDidMount() {
if (!this.props.isAuthenticated) {
return;
}
try {
const notes = await API.get("notes", "/notes");
const group = await API.get("group", "/group");
this.setState({ group , notes, isLoading: false});
} catch (e) {
}
Finally worked out what the issue was. I'd left a
*/}
in the middle of the renderGroup() method which is called by render() - (line 86)