I've stored url and a token in state in Parent component. I'm passing an url and a token as props from parent Component to child Component. However, if there is some event in parent Component, setState() is triggered and as a result, componentDidUpdate() of child Component gets executed.
As componentDidUpdate() was causing an infinite loop (as it triggers setState() inside child component), I've placed condition. But this does not prevent the error.
Child Component ie DisplayRevenue is as follows:
import React, { Component } from 'react';
import '../App.css';
import ListData from './listdata.js'
var axios = require('axios');
class DisplayRevenue extends Component {
constructor(props){
super(props);
this.state = { data:[], url:"" }
console.log(this.props.url);
}
componentWillMount() {
this.loadRevenue(this.props.url, this.props.token);
}
componentDidUpdate(){ //creates infinite loop
// console.log(this.props.url);
this.loadRevenue(this.props.url, this.props.token);
}
setData(data){
//if(this.state.url != this.props.url){
if(this.state.data != data.data){
console.log(data.data); //(1)
// console.log(this.state.url); //(2)
this.setState(data:data);
console.log(this.state.data); //(3)
// console.log(this.props.url); //(4)
} //(1) & (3) yields exactly same value so does (2) & (4)
}
loadRevenue(url,token){
axios({
method:'get',
url:url,
headers: {
Authorization: `Bearer ${token}`,
},
})
.then( (response) => {
// console.log(response.data);
this.setData(response.data);
})
.catch(function (error) {
console.log("Error in loading Revenue "+error);
});
}
render() {
return (
<ListData data={this.state.data}/>
);
}
};
export default DisplayRevenue;
Parent Component ie MonthToDate is as below:
import React, { Component } from 'react';
import '../App.css';
import DisplayRevenue from './displayRevenue'
var axios = require('axios');
class MonthToDate extends Component {
constructor(props){
super(props);
this.state = {
data:null,
url:"http://localhost:3000/api/monthtodate"
}
//console.log(this.props.location.state.token);
}
groupBySelector(event){
if ((event.target.value)==="invoice"){
this.setState({url:"http://localhost:3000/api/monthtodate"})
} else if ((event.target.value)==="customer") {
this.setState({url:"http://localhost:3000/api/monthtodate?group-by=customerNumber"})
} else if ((event.target.value)==="month") {
this.setState({url:"http://localhost:3000/api/invoices?group-by=month"})
} else {
this.setState({url:"http://localhost:3000/api/monthtodate"})
}
console.log(this.state.url);
}
render() {
return (
<div>
<select onChange={(event)=>this.groupBySelector(event)}>
<option value="invoice">GROUP BY INVOICE</option>
<option value="customer">GROUP BY CUSTOMER</option>
<option value="month">GROUP BY MONTH</option>
</select>
<DisplayRevenue url={this.state.url} token={this.props.location.state.token}/>
</div>
);
}
}
export default MonthToDate;
What am I missing?
Also, after I've received the url in the child component I want to render different component based on that url. For example <ListData /> component can handle only one type of url. How can I render another component within render() based on the url type??
You are calling an ajax call in componentDidUpdate, and you set the state on the callback, that will trigger another call and update which will call the ajax request again and callback will set state again and so on.
Your condition in setData:
if(this.state.data != data.data)
will always return true as objects are reference type and can't be compared, no matter what data returned from the ajax call it will always be a different object and will return true in your condition.
Example:
var obj1 = {a:1}
var obj2 = {a:1}
console.log(obj1 != obj2); // returns true
What you can do, is compare primitives values inside the two objects.
For example:
if(this.state.data.id != data.id) // id could be a string or a number for example
EDIT
Another thing i forgot to mention which may not relate to your problem directly but should be enforced, Never do ajax requests inside componentWillMount or the constructor for that matter, as the render function will be invoked before your ajax request will finish. you can read about it in the DOCS.
Ajax requests should be invoked in componentDidMount life cycle method instead.
EDIT #2
Another thing that can be helpful, in the MonthToDate render function you are passing a new instance of a function on each render (which may cause a performance hit)
<select onChange={(event)=>this.groupBySelector(event)}>
Try changing it to this (the event will be passed automatically to the handler):
<select onChange={this.groupBySelector}>
You would also need to bind it in the constructor:
constructor(props){
super(props);
this.state = {
data:null,
url:"http://localhost:3000/api/monthtodate"
}
//console.log(this.props.location.state.token);
this.groupBySelector = this.groupBySelector.bind(this); // binds this to the class
}
Related
Overview:
I'm using React and need to know from inside of a child component, when the parent components fetch call has been completed.
The trick is, I must have access to this knowledge inside the child from outside the render method (e.g. componentDidMount() ).
Background info:
I need to do this that way I can call a function (which I'm importing), since:
1) the function I want to call will update the state, thus causing an infinite render loop if called inside render
AND
2) the reason why I need to verify the fetch call has been made is because I'm passing the data from the fetch call (via context) into the function as parameters
I make a fetch call in my code and have a variable named "fetched" which is saved as "false" until the fetch is complete, then "fetched" is saved as "true".
Inside the promise, I setTimeout for a second because otherwise the promise will execute before the fetch call is completed, thus the promise will execute before "fetched" can be updated to true. I chose to only "resolve" and never "reject" because I need to know when the fetch call has been completed. So, the "resolve" of the promise is nested in a conditional statement (which returns true, if "fetched" is true).
Situation:
1) Among other state values, I have a state property named "fetch" defaulted as "false". The purpose of this state property is to evaluate as "true" once the fetch call has been completed.
2) In my App.js file, I make a fetch call to a database.
3) Inside the ".then" chain, once the fetch call has been accurately made. I then set the state property ("fetched") to true.
4) I have a method named "checkFetch" which return a promise. The promise only resolves and it will only resolve if "fetched" is true, also, everything inside the promise is wrapped in a setTimeout for 1 second (that way I give time for the "fetched" property to be updated).
5) In render(), I pass the "checkFetch" method to context (to be accessed by a child component).
6) In the ViewSchedule.js file (child), I make componentDidMount() an async function. Inside ComponentDidMount I await "checkFetch()" using context.
QUESTION:
This is my issue: if I don't set a timeout inside the promise, then nothing happens (as expected) because the fetch call hasn't been completed so the conditional statement is false.
Is there a better way of implementing this? For instance, I can set the timeout for x amount of seconds, but what if the fetch call were to take longer to complete? That seems like a dangerous practice.
Below is the code to reference.
App.js
import React, {Component} from 'react';
import {Route, Switch , NavLink} from 'react-router-dom';
import './App.css'
import InfoContext from '../InfoContext';
import ViewSchedule from '../ViewSchedule /ViewSchedule ';
class App extends Component {
constructor(props){
super(props);
this.state = {
business: [],
hours: [],
fetched: false,
};
}
static contextType = InfoContext;
checkFetch = () => {
return new Promise( (resolve,reject) => {
console.log("waiting...");
setTimeout(() => {
if(this.state.fetched === true){
resolve(true);
}
}, 1000);
});
}
componentDidMount() {
this.fetchDatabase();
}
fetchDatabase = () => {
Promise.all([
fetch(`http://localhost:8000/all`,
{
headers: {
'table':'business'
}
}),
fetch(`http://localhost:8000/all`,
{
headers: {
'table':'hours'
}
}),
])
.then(([business, hours]) => {
console.log('responses received!');
if (!business.ok)
return business.json().then(e => Promise.reject(e));
if (!hours.ok)
return hours.json().then(e => Promise.reject(e));
return Promise.all([business.json(), hours.json()]);
})
.then( ([business, hours]) => {
//fetch has been completed and the state has been updated so set "fetched" to true
this.setState({business, hours, fetched: true});
})
.catch(error => {
console.error({error});
});
}
render(){
return (
<InfoContext.Provider value={{businessData: this.state.business,
dayData: this.state.hours, fetched: this.state.fetched,
/* METHODS */
checkFetch: this.checkFetch}}>
<div className="container">
<main role="main">
{/* MAIN TEXT SECTION */}
<Switch>
//deleted all the nonessential routes/imports/methods so that this wouldn't be cluttered
<Route exact path='/view' component={ViewSchedule } />
</Switch>
</main>
</div>
</InfoContext.Provider>
);
}
}
export default App;
ViewSchedule.js
import React from 'react';
import { NavLink } from 'react-router-dom';
import { Container, Row, Col } from 'react-grid-system';
import './ViewSchedule.css';
import {InfoContext } from '../InfoContext';
//require function "call"
const logic = require('../test');
class ViewSchedule extends React.Component{
static contextType = InfoContext;
async componentDidMount(){
try{
let fetched = await this.context.checkFetch();
// call(this.context.business)
} catch (err){
console.log('ERROR in PROMISE: ',err)
}
}
render(){
return(
<div className='grid-container'>
<p>Content removed for testing purposes</p>
</div>
);
}
}
export default ViewSchedule;
InfoContext.js
import React, { createContext, Component} from 'react';
export const InfoContext = createContext({
businessData: null,
employeeData: null, dayData: null,
laborData: null,
scheduleData: null,
fetched: null,
checkFetch: () => {
},
updateEmployees: () => {
},
});
export default InfoContext;
If you need to alert a child when something happens in the parent, you use refs.
Here's a simple example code that will help you understand this pattern:
class ChildComponent extends React.Component {
onParentFetchFinished(response) {
// This function is called when parent component finished fetching
}
render() {
return null;
}
}
class ParentComponent extends React.Component {
constructor() {
super();
this.childComponentRef = React.createRef();
}
onClick = () => {
fetch('someurl').then(response => {
this.childComponentRef.current.onParentFetchFinished(response);
});
}
render() {
return (
<div>
<button onClick={this.onClick}>Start fetching</button>
<ChildComponent ref={this.childComponentRef} />
</div>
);
}
}
Whenever setState() is called, the component doesn't seem to rerender. As you can see by my comments, the state does in fact change and render seems to be called again, but if I don't add that if statement and simply add a paragraph tag that displays the data it will give me an error. I'm sure I'm missing something simple, but any help is appreciated.
import React from "react";
import axios from "axios";
import { constants } from "../constants/constants";
const { baseURL, apiKey, userName } = constants;
class User extends React.Component {
constructor(props) {
super(props);
this.state = {
user: []
};
}
componentDidMount() {
let getUserInfo = axios.create({
baseURL,
url: `?
method=user.getinfo&user=${userName}&api_key=${apiKey}&format=json`
});
getUserInfo().then(response => {
let data = response.data;
console.log(data.user.playcount); //logs second, displays correct
this.setState(state => ({
user: data
}));
});
}
render() {
console.log(this.state); //logs first and third, doesn't work on first but does on third
let toReturn;
if (this.state.user.length > 0) {
toReturn = <p>{this.state.user.user.playcount}</p>;
} else {
toReturn = <p>didn't work</p>;
}
return <div>{toReturn}</div>;
}
}
export default User;
React LifeCycle function sequence is Constructor and then it calls render method.
In constructor method it initialises the state which is currently empty user array.
Now it calls render() method as this.state.user is an empty array, referencing something out of it gives an error
this.state.user.user.playcount
this will generate an error if you dont have if condition.
After the first render it will call componentDidMount, now you fetch something update state. As setState occurred, render will be called again Now you have something in this.state.user then displaying will happen.
this.state.user.length > 0 is true
Look at this: https://reactjs.org/docs/react-component.html and https://reactjs.org/docs/conditional-rendering.html
You can right in single tag using conditional render like this
<p>{this.state.user.length ? this.state.user.user.playcount : 'loading'}
Hope this helps.
I think your problem might have something to do with the changing shape of the user value. You initialise the value to an empty array, but then—after the fetch is done—you assume it's an object (by using user.user).
Maybe you could simplify the code a bit to look more like the one below?
/* imports */
class User extends React.Component {
constructor(props) {
super(props);
this.state = {
user: null // Make it explicit there's no value at the beginning.
};
}
componentDidMount() {
let getUserInfo = axios.create(/* ... */);
getUserInfo().then(response => {
let data = response.data;
this.setState({ // No need to for a setter function as you dno't rely on the previous state's value.
user: data.user // Assign the user object as the new value.
});
});
}
render() {
let toReturn;
// Since it's now a `null`, you can use a simple existence check.
if (this.state.user) {
// User is now an object, so you can safely refer to its properties.
toReturn = <p>{this.state.user.playcount}</p>;
} else {
toReturn = <p>No data yet.</p>;
}
return <div>{toReturn}</div>;
}
}
export default User;
In the React component's componentDidMount() I make an axios get request to receive a response and setState to the component. The response is correct, and when print out the component object in the component class with this, the object looks good. Then I call console.log(this.state), then every property of the component become empty. Why this happens? How can I get the state's property?
MyComponent.js
React component did mount method:
componentDidMount() {
getLastWeek(this); // here I make a get request
console.log('===');
console.log(this); // this line prints out an object will all the properties
console.log(this.state); // all properties of state disappear
}
The get request used above:
service.js
...
function getLastWeek(component) {
const lastWeek = getEndpoint(7);
Axios.get(lastWeek)
.then(res => {
const bpi = res.data.bpi;
const prices = Object.values(bpi);
component.setState({ lastWeek: prices });
})
.catch(err => {
console.log(err);
});
}
...
You are making an axios request which is an asynchronous function, so what is happening is you are using console.log(this.state) before the state gets set.
The render() method gets executed every time the state changes so if you put your console.log inside the render() method you should now see how your state change. Something like this:
class Example extends Component {
constructor() {
...
}
componentDidMount() {
...
}
render() {
console.log(this.state);
return(...);
}
}
Hey I'm new is react my requirement is that when a user clicks on a button an ajax get request get fired to the
server and based of receieved response I have to prepare the html and display it.
below is my code it is not working .. it can be solved in jquery by using async: false but i don't have to use that
any idea how to solve using axios
import React from "react";
import axios from "axios"
class UserItems extends React.Component {
constructor(props) {
super(props);
this.state = {
useritem: ''
}
}
prepareHtmlOnAjaxResponse(){
var user_item = this.state.useritem
// html prepation done here
return preparedHtml;
}
getDatFromServeronclick() {
// getting user data from server is done in this function
// when data is receieved it is stored in a state
var self = this;
var promise = axios.get("http://localhost:4000/user/1/items.json")
promise.then(function (response) {
self.setState({ useritem: response.data.items })
self.prepareHtmlOnAjaxResponse() // prepare html
})
console.log("executing first and returning null")
}
render() {
var result = this.getDatFromServeronclick() // getting undefined value this has to be called onclick
return (
<div>
{result} / result is undefined
</div>
);
}
}
export default UserItems;
You have to use self.setState function instead of self.state assignment, otherwise React wouldn't trigger rerender of the component.
var promise = axios.get("http://localhost:4000/user/1/items.json")
promise.then(function (response) {
self.setState({ useritems: response.data.items })
})
From React's documentation:
NEVER mutate this.state directly, as calling setState() afterwards may replace the mutation you made. Treat this.state as if it were immutable.
Then in your render function
<button onClick={() => this.getDatFromServeronclick() }> {this.state.useritems.map(user => user.title)} </button>
you can replace user.title with whatever keys your object useritems has.
I have a React component that displays information about an entity. The id of the entity is passed in via a property. The component starts an AJAX call in "componentDidMount" to fetch the entity and updates state when the call completes/fails.
This works fine except that the component does not fetch new data when the entity id changes (via props).
I have tried starting a call in "componentWillReceiveProps" but at that stage the component still has is old properties set. I would have to pass nextProps to the AJAX call method and that doesn't seem right.
What is the best/cleanest way to have a component asynchronously update its state in response to a property change?
I'm new to react as well, so the Flux architecture is a bit intimidating to me. I'm doing it exactly as you said, using componentWillMount to load initial data via AJAX, and then componentWillReceiveProps with nextProps to load new data again if/when props change:
var Table = React.createClass({
getInitialState: function() {
return { data: [] };
},
componentWillMount: function(){
this.dataSource();
},
componentWillReceiveProps: function(nextProps){
this.dataSource(nextProps);
},
dataSource: function(props){
props = props || this.props;
return $.ajax({
type: "get",
dataType: 'json',
url: '/products?page=' + props.page + "&pageSize=" + props.pageSize
}).done(function(result){
this.setState({ data: result });
}.bind(this));
},
render: function() {
return (
<table className="table table-striped table-bordered">
<Head />
<Body data={this.state.data}/>
</table>
);
}
});
The hooks componentWillMount and componentWillReceiveProps has been deprecated since React v16.3.0 (source).
AJAX request should be done at the componentDidMount hook when you need to load data inmediatelly after the component is first rendered (source). When you wish to refresh the data after some props changed, you must use the componentDidUpdate hook.
But you'll have to take hand of another three lifecycle hooks to avoid starting an infinite loop of requests/updates. Assuming you wish to update a posts list based on props.category changes:
state should have two properties, category and currentCategory, set null on the component's constructor;
getDerivedStateFromProps is needed to update state.category from the new props.category;
shouldComponentUpdate is needed to compare both state.category and state.currentCategory to determine if the component should be updated;
getSnapshotBeforeUpdate is needed to determine if componentDidUpdate should make an AJAX request or to change the state.currentCategory value and finish the updating cycle.
Complete code would look like this (source):
import React, { Component, Fragment } from 'react';
import axios from "axios";
class Post extends Component {
constructor(props) {
super(props);
this.state = {
posts: [],
category: null,
currentCategory: null
};
this._createMarkup = this._createMarkup.bind();
}
static getDerivedStateFromProps(props, state) {
if (props.category !== state.category) {
return {
category: props.category
};
}
return null;
}
componentDidMount() {
this._fetchData();
}
shouldComponentUpdate(nextProps, nextState) {
return this.state.currentCategory !== nextState.category;
}
getSnapshotBeforeUpdate(prevProps, prevState) {
return prevState.currentCategory !== prevState.category;
}
componentDidUpdate(prevProps, prevState, dataDidFetch) {
// dataDidFetch is returned by getSnapshotBeforeUpdate
if (dataDidFetch) {
this.setState({
currentCategory: this.state.category
});
} else {
this._fetchData();
}
}
_fetchData() {
const category = this.state.category;
axios.get(`/some/api/endpoint?category=${category}`).then(posts => {
this.setState({
posts: posts.data
});
});
}
_createMarkup(html) {
return { __html: html };
}
render() {
return (
<Fragment>
{this.state.posts.map(post => (
<article className="post" key={post.id}>
<h2>{post.title.rendered}</h2>
<div dangerouslySetInnerHTML={this._createMarkup( post.content.rendered )} />
<p className="post-link">
{post.resource_link_label}
</p>
</article>
))}
</Fragment>
);
}
}
export default Post;