React simple fetch program run into an infinite loop - javascript

I have a simple program which receives some JSON data from a node backend and set received data into state. The problem is it reset state infinite times, creating an infinite rendering.
Here is the JSON data
[
{
"id": 1,
"name": "Product 1",
"category": "C1",
"price": "100"
},
{
"id": 2,
"name": "Product 2",
"category": "C1",
"price": "80"
},
{
"id": 3,
"name": "Product 3",
"category": "C3",
"price": "120"
}
]
Here is the react program.
import React, { useState } from 'react'
const MainApp = () => {
const [products, setProducts] = useState([])
fetch("http://localhost:5000/products")
.then((res) => res.json())
.then((res) => {setProducts(res)})
.catch((err) => console.error(err))
console.log("Products:",products) //This keep getting logged forever.
return (
<h1>Test</h1>
)
}
export default MainApp
What have I done wrong?

The fetch is continuously performed on every render of MainApp. Consider using an effect to solve this.

You should only call fetch when components mounts. Since you are using hooks, you should use `
useEffect(()=> {
fetch("http://localhost:5000/products")
.then((res) => res.json())
.then((res) => {setProducts(res)})
.catch((err) => console.error(err))
}, [])
`
What you are doing right now, is calling fetch on every render. Imagine it like this. You are rendering the component, during that you are fetching something and updating the state. When the state updates, it rerenders the components and you are going on an infinite loop.

the problem is in {setProducts(res)} this will update the state and re-render the component then call the fetch function second time and so on

Related

useEffect() and array of deeply nested objects

I am trying to call useEffect() whenever arrayWithDeeplyNestedObjects changes. export default compose(... is part of an offline first database (watermelonDB) and updates arrayWithDeeplyNestedObjects when there is a change in the database. One could expect useEffect() to execute, whenever arrayWithDeeplyNestedObjects changes, but it is not. This is beause useEffect() only performs a shallo comparison and does not recognize the changes in arrayWithDeeplyNestedObjects.
import withObservables from '#nozbe/with-observables';
import {withDatabase} from '#nozbe/watermelondb/DatabaseProvider';
import {compose} from 'recompose';
import {Q} from '#nozbe/watermelondb';
const Foo = ({arrayWithDeeplyNestedObjects}) => {
console.log('render'); // <-- this renders whenever arrayWithDeeplyNestedObjects is updated
useEffect(() => {
console.log(new Date()); // <-- this does not render whenever arrayWithDeeplyNestedObjects is updated
const doSomething = async () => {
...
};
const data = await doSomething();
setData(data);
}, [arrayWithDeeplyNestedObjects]); // <-- this does only perform a shallow compare
return <SomeNiceUi />
}
export default compose(
withDatabase,
withObservables(['arrayWithDeeplyNestedObjects'], ({database}) => ({
arrayWithDeeplyNestedObjects: database.get(SOME_TABLE).query().observe(),
})),
)(Foo); <-- subscription das fires everytime an update is made to the database
This is how arrayWithDeeplyNestedObjects looks like
[{"__changes": null, "_isEditing": false, "_preparedState": null, "_raw": {"_changed": "x,y", "_status": "created", "id": "3", "x": 5851, "id_arr": "[\"160\"]", "id": "6wmwep5xwfzj3dav", "y": 0.17576194444444446}, "_subscribers": [], "collection": {"_cache": [RecordCache], "_subscribers": [Array], "changes": [Subject], "database": [Database], "modelClass": [Function SomeTable]}}]
The changes to arrayWithDeeplyNestedObjects are done in the objects either to x, y or id_arr. The length of arrayWithDeeplyNestedObjects can change as well. There might be more (or less) objects of the same structure in there.
How to call useEffect() everytime arrayWithDeeplyNestedObjects changes?
try this:
useEffect(() => {
console.log(new Date());
const doSomething = async () => {
...
};
doSomething();
}, [JSON.stringify(arrayWithDeeplyNestedObjects)]);

ReactJS - map doesn't work for single item

I have a really trivial problem with map function. I am making a request to my API, which returns the single object. I then want to display the object in render method.
const api = axios.create({
baseURL: `https://localhost:5001/api/v1/`
})
class SinglePost extends Component {
state = {
post: []
}
constructor(props) {
super(props);
api.get(`posts/9a6b5be6-b3ef-4c2d-a36b-08da14c62914`).then(res => {
console.log(res.data)
this.setState({ posts: res.data })
})
}
render() {
return (
<div>
{
this.state.post.map(x => <h2>{x.title}</h2>)
}
</div>
)
}
}
export default SinglePost;
The console.log() of res.data seems to display the data just fine.
{
"id": "9a6b5be6-b3ef-4c2d-a36b-08da14c62914",
"name": null,
"title": "Teeest",
"urlTitle": null,
"content": "123",
"created": "2022-04-02T18:30:55.1536762",
"description": "123",
"score": 0,
"isDeleted": false,
"currentFlagStatus": 0,
"flagReason": null,
"userId": "9ecac069-8cfc-4cac-8056-87093fb9c57c",
"authorName": null,
"authorProfilePic": null,
"hashtags": [
{
"id": "a8bc782c-7f5e-4dfc-220c-08da1355d3ec",
"hashtagName": "byq",
"hashtagNameInLower": "byq",
"amountOfHashtagFollowers": 0
},
{
"id": "a5efd6b1-cff0-40b5-2218-08da1355d3ec",
"hashtagName": "Test",
"hashtagNameInLower": "test",
"amountOfHashtagFollowers": 0
}
],
"comments": []
}
However my map function doesn't seem to put the data inside the <h2> tag. Setting the breakpoint exactly at this.state.post.map(x => <h2>{x.title}</h2>) showed that it's not even being hit, however there are no errors on the console. How can I properly map this?
EDIT:
Just like Alon Barenboim and Nicholas Tower pointed out, there was a typo (setting state to posts instead of post). However, the issue now is that that piece of code now throws the error that this.state.post.map is not a function.
I guess that the issue is that you call this.setState({ posts: res.data }) for posts, but trying to map post: this.state.post.map(x => <h2>{x.title}</h2>
EDIT:
If I understand correctly, you just need to display the data that is stored inside the posts object in a component.
In order to do so, you can just do the following:
const api = axios.create({
baseURL: `https://localhost:5001/api/v1/`
})
class SinglePost extends Component {
state = {
posts: []
}
constructor(props) {
super(props);
api.get(`posts/9a6b5be6-b3ef-4c2d-a36b-08da14c62914`).then(res => {
console.log(res.data)
this.setState({ posts: res.data })
})
}
render() {
return (
<div>
{this.state.posts ? <h2>this.state.posts.title</h2> : null}
</div>
)
}
}
export default SinglePost;
You can obtain each value that is stored inside the state object just by writing {this.state.posts.ATTRIBUTE}

I keep getting this TypeError: Cannot read property 'map' of undefined

I am trying to render this data from the API onto my page.
I am aware that what I am trying to render isn't in the typical array its a javascript object which is throwing me off.
With my code, as is, the API data is there its just a matter of being able to access because .map isn't working because the data I have is not an array.
Do I need to create another function?
I am not sure
import React from 'react';
import './App.css';
class App extends React.Component {
state = {
apiData: []
}
render() {
console.log('api datat is')
return (
<div>
<center>
<h1>hello something</h1></center>
{this.state.apiData.map(title => title.rendered)}
</div>
)
}
componentDidMount() {
fetch('http://www.mocky.io/v2/5dece3d333000052002b9037')
.then(response => response.json())
.then(data => {
this.setState({
apiData: data.data
})
})
console.log("component fetched data")
}
}
export default App
Any help would be great.
I am still learning how to code so be nice
Look at the API response:
{
"id": 233383,
"date": "2019-10-28T10:50:53",
"date_gmt": "2019-10-28T10:50:53",
"modified": "2019-10-28T10:55:14",
"modified_gmt": "2019-10-28T10:55:14",
"link": "https:\/\/www.stylist.co.uk\/long-reads\/friendship-friends-whatsapp-facebook-messenger-social-media-group-chat-best-friends-psychology-advice\/233383",
"title": {
"rendered": "Whatsapp and Facebook Messenger: how group chat is changing the dynamic of our friendships"
},
"slug": "whatsapp-and-facebook-messenger-how-group-chat-is-changing-the-dynamic-of-our-friendships",
etc.
It does not have a .data property, so
.then(data => {
this.setState({
apiData: data.data
})
})
sets undefined to this.state.apiData.
The only rendered exists in the one top-level object in the response, so you should remove the .map entirely, something like:
.then(data => {
this.setState({
apiData: data
})
})
<h1>hello something</h1></center>
{this.state.apiData.title.rendered}

Problem with using deeper nested data fetched from API

I encountered a problem when I try to fetch some data from PokeAPI. Here's my code for PokemonCard component.
import React, { useEffect, useState } from "react";
import axios from "axios";
const PokemonCard = ({ pokemonID }) => {
const [pokemon, setPokemon] = useState({});
useEffect(() => {
(async () => {
const result = await axios.get(
`http://pokeapi.co/api/v2/pokemon/${pokemonID + 1}`
);
setPokemon(result.data);
})();
// console.log(pokemon.weight)
}, [pokemonID]);
return (
<div className="pokemon">
{pokemon.sprites.front_default}
</div>
);
};
export default PokemonCard;
Everything works properly when I try to reach data like: pokemon.weight or pokemon.base_experience. But I get errors when I try to use some deeper nested variables.
pokemon.sprites.front_default gives me an error TypeError:
Cannot read property 'front_default' of undefined.
Here's a sample of data from API:
"name": "bulbasaur",
"order": 1,
"species": {
"name": "bulbasaur",
"url": "https://pokeapi.co/api/v2/pokemon-species/1/"
},
"sprites": {
"back_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/1.png",
"back_female": null,
"back_shiny": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/1.png",
"back_shiny_female": null,
"front_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png",
"front_female": null,
"front_shiny": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/1.png",
"front_shiny_female": null
},
"stats": [
{
"base_stat": 45,
"effort": 0,
"stat": {
"name": "hp",
"url": "https://pokeapi.co/api/v2/stat/1/"
}
}
],
"types": [
{
"slot": 2,
"type": {
"name": "poison",
"url": "https://pokeapi.co/api/v2/type/4/"
}
},
{
"slot": 1,
"type": {
"name": "grass",
"url": "https://pokeapi.co/api/v2/type/12/"
}
}
],
"weight": 69
PS. Is it a good practice to make about 150 separate calls to API in every child component? Or should I somehow do it with one call? Thank you.
You were trying to access a key inside an undefined key of pokemon variable. Please check the updated line where you are actually rendering.
{pokemon.sprites ? pokemon.sprites.front_default : ''}
As Pokemon is an empty object before the api fetches the data and
updates to the state, so pokemon.sprites is actually undefined.
import React, { useEffect, useState } from "react";
import axios from "axios";
const PokemonCard = ({ pokemonID }) => {
const [pokemon, setPokemon] = useState({});
useEffect(() => {
(async () => {
const result = await axios.get(
`http://pokeapi.co/api/v2/pokemon/${pokemonID + 1}`
);
setPokemon(result.data);
})();
// console.log(pokemon.weight)
}, [pokemonID]);
return (
<div className="pokemon">
//this should work for you
{pokemon.sprites ? pokemon.sprites.front_default : ''}
</div>
);
};
export default PokemonCard;
Gets much easier in Optional chaining (?.)
pokemon.sprites?.front_default

How to take a jest snapshot after axios fetched data in componentDidMount?

Component to test
class Carousel extends React.Component {
state = {
slides: null
}
componentDidMount = () => {
axios.get("https://s3.amazonaws.com/rainfo/slider/data.json").then(res => {
this.setState({ slides: res.data })
})
}
render() {
if (!slides) {
return null
}
return (
<div className="slick-carousel">
... markup trancated for bravity
</div>
)
}
}
export default Carousel
Test
import React from "react"
import renderer from "react-test-renderer"
import axios from "axios"
import Carousel from "./Carousel"
const slides = [
{
ID: "114",
REFERENCE_DATE: "2018-07-02",
...
},
{
ID: "112",
REFERENCE_DATE: "2018-07-06",
...
},
...
]
jest.mock("axios")
it("", () => {
axios.get.mockImplementationOnce(() => Promise.resolve({ data: slides }))
const tree = renderer.create(<Carousel />).toJSON()
expect(tree).toMatchSnapshot()
})
snapshot only records null, since at the moment of execution I suppose state.slides = null.
Can't put my finger on how to run expectations after axios done fetching the data.
Most of the samples online either use enzyme, or show tests with async functions that return promises. I couldn't find one that would show example only using jest and rendered component.
I tried making test function async, also using done callback, but no luck.
in short:
it("", async () => {
axios.get.mockImplementationOnce(() => Promise.resolve({ data: slides }))
const tree = renderer.create(<Carousel />);
await Promise.resolve();
expect(tree.toJSON()).toMatchSnapshot()
})
should do the job
in details: besides you have mocked call to API data is still coming in async way. So we need toMatchSnapshot call goes to end of microtasks' queue. setTimeout(..., 0) or setImmediate will work too but I've found await Promise.resolve() being better recognizable as "everything below is coming to end of queue"
[UPD] fixed snippet: .toJSON must be after awaiting, object it returns will never be updated
The accepted answer started to fail the next day. After some tweaking, this seems to be working:
import React from "react"
import renderer from "react-test-renderer"
import axios from "axios"
import Carousel from "./Carousel"
jest.mock("axios")
const slides = sampleApiResponse()
const mockedAxiosGet = new Promise(() => ({ data: slides }))
axios.get.mockImplementation(() => mockedAxiosGet)
// eventhough axios.get was mocked, data still comes anychrnonously,
// so during first pass state.slides will remain null
it("returns null initally", () => {
const tree = renderer.create(<Carousel />).toJSON()
expect(tree).toMatchSnapshot()
})
it("uses fetched data to render carousel", () => {
const tree = renderer.create(<Carousel />)
mockedAxiosGet.then(() => {
expect(tree.toJSON()).toMatchSnapshot()
})
})
function sampleApiResponse() {
return [
{
ID: "114",
REFERENCE_DATE: "2018-07-02",
...
},
{
ID: "114",
REFERENCE_DATE: "2018-07-02",
...
},
]
}

Categories