Rendering loop error with react axios setState - reactjs

I have different ingredients(vodka, gin, whiskey...) json files in a dummy folder.
I have an IngredientList.js where I select one ingredient and pass it down to
IngredientSearch.js
The IngredientSearch.js gets the relevant json file based on the ingredient name and then I set the state of ingredientRes to the res.data.drinks
Problem I am getting is that when I print the console.log(newVals) --> the console logs the arrays from the json infinitely. Seems like I am rerendering something infinitely.
What is wrong with my setup?
IngredientSearch.js:
import React, { Component } from 'react';
import axios from 'axios';
class IngredientSearch extends Component {
constructor() {
super();
this.state = {
ingredientRes: []
};
}
componentDidUpdate(prevProps) {
let ingredient = this.props.ingredient; //for example: vodka
this.getIngredient_drinks(ingredient);
}
getIngredient_drinks = (ingredient) => {
if(ingredient !== null) {
axios.get(`../dummy/${ingredient}.json`)
.then((res)=>{
let newVals = [];
newVals.push(res.data.drinks);
//console.log(newVals); // keeps relogging the arrays
this.setState({ ingredientRes: newVals });
}).catch((err)=>{
console.log(err);
})
}
}
render() {
return (
<div>
IngredientSearch Results
I want to map the ingredientRes here
</div>
)
}
}
export default IngredientSearch;

You may call setState() immediately in componentDidUpdate() but note that it must be wrapped in a condition like -
if (this.props.ingredient !== prevProps.ingredient) {
this.getIngredient_drinks(ingredient);
}
Otherwise it will cause an infinite loop.
For reference - https://reactjs.org/docs/react-component.html#componentdidupdate

Related

I can't get my API data to render with the map function React.js

The API does not render and it says this.state.weather.map is not a function. I need help, I have tried different ways of mapping through but does not change the outcome. Any feedback?
import React from 'react';
import axios from 'axios';
export default class Weather extends React.Component {
constructor(props) {
super(props)
this.state = {
weather: []
}
}
async componentDidMount(){
axios.get("http://api.weatherapi.com/v1/current.json?key=?=nyc").then(res => {
console.log(res);
this.setState ({weather: res.data})
});
}
renderWeather(){
this.state.weather.map(weather => {
return (
<p>{weather.location.name}</p>
)
})
}
render () {
return (
<div>
{this.renderWeather()}
</div>
)
}
}
renderWeather(){
return this.state.weather.map(weather => {
return(
<p>{weather.location.name}</p>
)
})
}
you have missed the return statement inside the renderWeather function, above snippet works for you
The API returns JSON.
this.setState ({weather: res.data})
Check the typeof res.data. Is is probably not an array but an object, hence this.state.weather.map is not a function, because Array.map() is an array method.
Make sure that you set the state properly to an array and it should work just fine..

TypeError: Cannot read property 'rocket_name' of undefined -- Even though it is defined

I'm using the SpaceX API to build a personal project. I'm using React Router to dynamically load components, rather than refreshing the whole website.
Here is my LaunchDetails component where I'm trying to output some data:
import React, { Component } from 'react'
class LaunchDetail extends Component {
state = {
launch: []
}
async componentDidMount () {
try {
const res = await fetch(`https://api.spacexdata.com/v3/launches/${this.props.match.params.flight_number}`)
const data = await res.json()
this.setState({
launch: data,
rocket: data.rocket
})
} catch (e) {
console.log(e)
}
}
render () {
const { launch, rocket } = this.state
console.log(rocket)
return (
<div>
<div>
<h1>{launch.mission_name}</h1>
<p>SpaceX Flight Number: {launch.flight_number}</p>
<p>Launched: {launch.launch_year}</p>
<p>Rocket: {rocket.rocket_name}, {rocket.rocket_type}</p>
</div>
</div>
)
}
}
export default LaunchDetail
Data one level deep like launch.mission_name is displaying correctly... However, when I try and go down another level, say, rocket.rocket_name (eg: launch.rocket.rocket_name), it throws the above error.
What is strange is that this works in another component, but that is using a different end point (all the data) and I'm mapping through it. Not sure if the way I'm calling the data in this component is to blame or not...
Does anyone have any idea why this could be happening?
EDIT: I've updated the code to be simpler after receiving some comments, error still persists:
import React, { Component } from 'react'
class LaunchDetail extends Component {
state = {
launch: []
}
async componentDidMount () {
try {
const res = await fetch(`https://api.spacexdata.com/v3/launches/${this.props.match.params.flight_number}`)
const data = await res.json()
this.setState({
launch: data
})
} catch (e) {
console.log(e)
}
}
render () {
const { launch } = this.state
return (
<div>
<div>
<h1>{launch.mission_name}</h1>
<p>SpaceX Flight Number: {launch.flight_number}</p>
<p>Launched: {launch.launch_year}</p>
<p>Rocket: {launch.rocket.rocket_name}, {launch.rocket_type}</p>
</div>
</div>
)
}
}
export default LaunchDetail
From here you can find that the react lifecycle methods go in the following order.
componentWillMount --> render --> componentDidMount
You have initialized state with
state = {
launch: []
}
So on the first render, state.rocket will be undefined.
To fix this, either initialise state.rocket or change componentDidMount to componentWillMount
With the former being prefered.
Note componentWillMount is deprecated in version 17.
After OP's edit. You are still initializing launch to []
On the first render launch.rocket will be undefined. Therefore launch.rocket.rocket_name will throw an error.
Either initialise launch to have a rocket field. Or do something like
(launch.rocket || {}).rocket_name, or something else to check that rocket is defined before accessing rocket_name.

Infinite Looping Errorr in ReactJS

Layout.js
import React, {Component} from "react"
import Aircraft from "./Aircraft"
class Layout extends Component {
constructor(props) {
super(props)
this.state = {
test: true,
acn: ""
}
}
WhatIsAircraftName = (acn) => {
this.setState({
acn: acn
})
}
render() {
return (
<div>
<div className="mainD posRel hidO">
<div className="posRel hidO topD">
</div>
<div className="posRel hidO bottomD container">
<Aircraft clk={this.WhatIsAircraftName} />
</div>
</div>
</div>
)
}
}
export default Layout
Aircraft.js
import React, {Component} from "react"
import Loading from "./Loading"
class Aircraft extends Component {
constructor(props) {
super(props)
this.state = {
aircraft: [],
loading: false,
utilized: 0
}
}
componentDidMount() {
let mod_this = this
this.setState({
loading: true
})
fetch("https://some-endpoint")
.then(function(response) {
if (response.ok) {
return response.json()
}
})
.then(function(myJson) {
mod_this.setState({
aircraft: myJson,
loading: false
})
})
}
DisplayAircraft() {
let ac = this.state.aircraft
this.props.clk(ac.data[0].ident)
return (
<div className="acD posRel hidO selected">
{
<h2>{ac.data[0].ident}</h2>
}
</div>
)
}
render() {
const {aircraft} = this.state
return (
<div className="posRel hidO leftD">
<h1>Aircrafts</h1>
{
!aircraft || aircraft.length <= 0 || this.state.loading ?
<Loading /> :
this.DisplayAircraft()
}
</div>
)
}
}
export default Aircraft
When I run my app, I get setState loop error:
Unhandled Rejection (Invariant Violation): Maximum update depth
exceeded. This can happen when a component repeatedly calls setState
inside componentWillUpdate or componentDidUpdate. React limits the
number of nested updates to prevent infinite loops.
The reason I am doing it like this is the Aircraft component will get the Aircraft ID which I want to send to another child component, hence I am sending it to the parent to use it as props for another component.
Section is:
Layout.WhatIsAircraftName [as clk]
The problem is that your render method is not pure and is updating state.
The render() function should be pure, meaning that it does not modify component state, it returns the same result each time it’s invoked, and it does not directly interact with the browser. - React Docs
In your parent component, you have WhatIsAircraftName which is calling setState. Whenever this method is invoked, it will trigger a state update which will trigger a render. In the parent's render you are passing the Aircraft component the prop clk which is assigned to WhatIsAircraftName. Then, in Aircraft's render, it's calling DisplayAircraft, which is invoking the prop clk which starts us back at the top.
Layout#render -> Aircraft#render -> DisplayAircraft -> this.props.clk -> WhatIsAircraftName -> this.setState -> Layout#render -> etc. (infinite loop).
This loop needs to be broken, setState should never be invoked within render.
Try something like this, I've moved the logic around that you are no longer updating the state from the render function.
import React, {Component} from "react"
import Loading from "./Loading"
class Aircraft extends Component {
constructor(props) {
super(props)
this.state = {
aircraft: [],
loading: false,
utilized: 0
}
}
componentDidMount() {
let mod_this = this
this.setState({
loading: true
})
fetch("https://some-endpoint")
.then(function(response) {
if (response.ok) {
return response.json()
}
})
.then(function(myJson) {
this.props.clk(myJson.data[0].ident)
mod_this.setState({
aircraft: myJson,
loading: false
})
})
}
render() {
const {aircraft} = this.state
return (
<div className="posRel hidO leftD">
<h1>Aircrafts</h1>
{
!aircraft || aircraft.length <= 0 || this.state.loading ?
<Loading /> :
(<div className="acD posRel hidO selected">
<h2>{aircraft.data[0].ident}</h2>
</div>)
}
</div>
)
}
}
export default Aircraft
This should at least get it working for you but based on what I can see in your code I do have another suggestion. It would be much simpler to do the API call from the Layout component and then pass the aircraft information down to the Aircraft component via Props. In your current code you are having to pass the data back up via a function you passed via a prop which makes it all a little more complicated. React tends to be much easier to work with when you are passing data down the chain.
Hope that helps!

When mapStateToProps in create create a new axios call

I have created a Reactjs component that receives a mapStateToProps function call. Everything works fine except the ajax call using Axios.
The class on a mapStateToProps update needs to call the server and add its payload to the state of the component and update the textarea.
The error I am getting from the console is,
ReactDOMIDOperations.js:47 Uncaught RangeError: Maximum call stack size exceeded
Below is what I have so far. Can anyone show me how to fix this issue?
import React from "react";
import { connect } from "react-redux";
import ApiCalls from "../../../utils/ApiCalls";
const mapStateToProps = state => {
return { passFilePath: state.passFilePath };
};
/**
* This component is a template to display
* widgets of information
*/
class IdeTextEditorClass extends React.Component {
constructor() {
super();
this.state = {
newData: [],
pathData: []
}
}
/**
* Received request from server add it to
* react component so that it can be rendered
*/
componentDidUpdate() {
try {
this.setState({ pathData: this.props.passFilePath[this.props.passFilePath.length - 1] });
} catch (err) {
this.setState({ pathData: '' });
}
console.log('path', this.state.pathData.data);
ApiCalls.readSassFile(this.state.pathData.data)
.then(function (serverData) {
this.setState({ newData: serverData[0].data })
}.bind(this));
}
render() {
try {
this.state.newData
} catch (err) {
this.setState({ newData: '' });
}
return (
<fieldset>
<input type="text" value={this.state.pathData.data} />
<textarea id="ide-text-area" name="ide-text-area" value={this.state.newData} /></fieldset>
)
}
}
const IdeTextEditor = connect(mapStateToProps)(IdeTextEditorClass);
export default IdeTextEditor;
class IdeTextEditorClass extends React.Component {
constructor() {
super();
/*
based on your original code it seems the default data should be empty string ,as you set them to be empty string when you cannot get data from server.
*/
this.state = {
newData: '',
pathData: ''
}
}
/**
* Received request from server add it to
* react component so that it can be rendered
*/
componentDidMount() {
try {
this.setState({ pathData: this.props.passFilePath[this.props.passFilePath.length - 1] });
} catch (err) {
this.setState({ pathData: '' });
}
console.log('path', this.state.pathData.data);
ApiCalls.readSassFile(this.state.pathData.data)
.then(function (serverData) {
this.setState({ newData: serverData[0].data })
}.bind(this));
}
render() {
//by default your newData is already empty string. so skip the checking here.
let path = this.state.pathData ? this.state.pathData.data : '';
return (
<fieldset>
<input type="text" value={path} />
<textarea id="ide-text-area" name="ide-text-area" value={this.state.newData} /></fieldset>
)
}
}
Explanation:
The major change is to change componentDidUpdate to componentDidMount.
Putting the data initializing logic in componentDidMount because:
called only once, thus avoiding the endless update loop mentioned in the comments. Also, initialization logic is usually expected here.
this method is called after initial render, so you can at least display something to user during the wait for data (from server). for example, in your render method, you can check newData and if it is not available, display a loading icon. Then React calls componentDidMount, and fetch your data -> update state -> trigger render again -> displays your input / text area using new data fetched from server. Of course, if you don't want to bother showing a loading icon, it is also fine, because your view will probably be updated quickly, when the ajax call returns.

Meteor - how to give tracker autorun a callback

I have a little piece of code that renders data from the database according to the path name. My only problem is that when I try to retrieve that data, using this.state.note._id it returns an error that says it cannot find _id of undefined. How would I access my object that is put into a state? It only gives the error when I try to access the items inside the object such as _id
import React from "react";
import { Tracker } from "meteor/tracker";
import { Notes } from "../methods/methods";
export default class fullSize extends React.Component{
constructor(props){
super(props);
this.state = {
note: [],
document: (<div></div>)
};
}
componentWillMount() {
this.tracker = Tracker.autorun(() => {
Meteor.subscribe('notes');
let note = Notes.find({_id: this.props.match.params.noteId}).fetch()
this.setState({ note: note[0] });
});
}
renderDocument(){
console.log(this.state.note);
return <p>Hi</p>
}
componentWillUnmount() {
this.tracker.stop();
}
render(){
return <div>{this.renderDocument()}</div>
}
}
I know that the reason it is returning undefined is because (correct me if I am wrong) the page is rendering the function before the the tracker could refresh the data. How would I get like some sort of callback when the tracker receives some data it will call the renderDocument function?
You're initializing your note state as an array but then you're setting it to a scalar later. You're also not checking to see if the subscription is ready which means that you end up trying to get the state when it is still empty. The tracker will run anytime a reactive data source inside it changes. This means you don't need a callback, you just add any code you want to run inside the tracker itself.
You also don't need a state variable for the document contents itself, your render function can just return a <div /> until the subscription becomes ready.
Note also that .findOne() is equivalent to .find().fetch()[0] - it returns a single document.
When you're searching on _id you can shorthand your query to .findOne(id) instead of .findOne({_id: id})
import React from "react";
import { Tracker } from "meteor/tracker";
import { Notes } from "../methods/methods";
export default class fullSize extends React.Component{
constructor(props){
super(props);
this.state = {
note: null
};
}
componentWillMount() {
const sub = Meteor.subscribe('notes');
this.tracker = Tracker.autorun(() => {
if (sub.ready) this.setState({ note: Notes.findOne(this.props.match.params.noteId) });
});
}
renderDocument(){
return this.state.note ? <p>Hi</p> : <div />;
}
componentWillUnmount() {
this.tracker.stop();
}
render(){
return <div>{this.renderDocument()}</div>
}
}

Resources