Render() state element is initially empty until api call - javascript

I have a class component which should display some list values after an API call, in my render function I call a function to populate some other state list (with specific object properties from the fetched list), the problem is that in the render call, the state values are initially empty and as such the component I return is also just empty.
I've tried using componentDidUpdate() but I dont have much of an idea on how to go about using it, it usually gives me an infinite loop.
Here is my relevant code:
class AdminSales extends React.Component {
constructor(props) {
super(props);
this.state = {
items: [],
data: [],
};
}
componentDidMount() {
this.fetchData();
}
fetchData() {
fetch("/api/items")
.then((res) => res.json())
.then((items) => this.setState({ items: items }));
}
componentDidUpdate(prevState) {
if (JSON.stringify(prevState.items) == JSON.stringify(this.state.items)) {
// Do nothing
} else {
// This gives infinite loop ...
// this.fetchData();
}
}
populateData() {
this.state.items.forEach(function (item) {
this.state.data.push({
name: item.name,
value: item.quantity,
});
}, this);
}
render() {
// Output shown line: 65
console.log(this.state);
this.populateData();
const { data } = this.state;
return ( ... );
}
}
export default AdminSales;
Any help will be much appreciated.

First of all there are multiple issues in your code
Updating/Mutating state in render
Instead of updating the state using setState, updating the state inplace in populateData method
Also, as #Drew mentioned we don't have to duplicate items into data instead we can store only the required info in the state once after getting the response from the API.
In the meantime while waiting for the response if you want to show a loading info you can do that as well.
Below is the example covering all those points mentioned above.
const mockAPI = () => {
return new Promise((resolve) => setTimeout(() => {
resolve([{id:1, name: "ABC", quantity: 1}, {id: 2, name: "DEF", quantity: 5}, {id: 3, name: "XYZ", quantity: 9}])
}, 500));
}
class AdminSales extends React.Component {
constructor(props) {
super(props);
this.state = {
items: [],
loading: true
};
}
componentDidMount() {
this.fetchData();
}
fetchData = () => {
mockAPI()
.then((res) => {
this.populateData(res);
});
}
populateData = (data) => {
this.setState({
items: data.map(({name, quantity}) => ({
name,
value: quantity
})),
loading: false
})
}
render() {
//console.log(this.state);
const { items, loading } = this.state;
return loading ? <p>Loading...</p> :
items.map(item => (
<div>{item.name}: {item.value}</div>
));
}
}
ReactDOM.render(<AdminSales />, document.getElementById("react"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Note: For simplicity I've mocked the backend API with simple setTimeout.

Related

Returning state after API call inside getDerivedStateFromProps

I have a react component that receives props for filtering from its parent. When the parent props change I use getDerivedStateFromProps in the child in the following code:
static getDerivedStateFromProps(props, state) {
if (props.filter === null) {
return state;
}
const { name, description } = props.filter;
ApiService.get("cards", { name: name, description: description })
.then(response => {
console.log("get request returned ", response.data);
return { cards: response.data };
}
);
}
in the console the response.data log is the approriate array of objects. However the state does not update and the rendering function still uses the old cards array and not the one that was received from the ApiService response. How do I make it so the cards array updates properly so that on the next render it will show the filtered cards?
getDerivedStateFromProps is not the correct lifecycle hook for this. You'll need to put the fetching code in componentDidMount and componentDidUpdate, and use this.setState once the data is available.
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
cards: null,
}
}
componentDidMount() {
this.loadData();
}
componentDidUpdate(prevProps) {
if (this.props.filter !== prevProps.filter) {
this.loadData();
}
}
loadData() {
const { name, description } = this.props.filter;
ApiService.get("cards", { name: name, description: description })
.then(response => {
this.setState({ cards: response.data });
});
)
render() {
if (!this.state.cards) {
return <div>Loading...</div>
}
return (
// some jsx using the cards
)
}
}

Console.log() shows undefined before getting data

I seem to have a lifecycle hook issue that I can't seem to solve.
export default class EditRevision extends Component {
state = {
data: [],
customColumns: []
}
componentWillMount = () => {
axios.get('http://localhost:8080/lagbevakning/revision/subscriptions?id=' + (this.props.match.params.id)).then(response => {
this.setState({
data: response.data,
loading: false
})
})
}
render() {
/* THIS IS THE CONSOLE.LOG() I AM REFERRING TO */
console.log(this.state.data.subscriptionRevisionDTOS)
return (
<div></div>
)
}
}
And this is my log upon rendering the component
https://i.gyazo.com/9dcf4d13b96cdd2c3527e36224df0004.png
It is undefined, then retrieves the data as i desire it to, then it gets undefined again.
Any suggestions on what causes this issue is much appreciated, thank you.
Replace this:
componentWillMount = () => {
axios.get('http://localhost:8080/lagbevakning/revision/subscriptions?id=' + (this.props.match.params.id)).then(response => {
this.setState({
data: response.data,
loading: false
})
})
with:
constructor(props){
super(props)
this.state = {
data: [],
customColumns: []
}
axios.get('http://localhost:8080/lagbevakning/revision/subscriptions?id=' + (this.props.match.params.id)).then(response => {
this.setState({
data: response.data,
loading: false
})
})
}
try to call axios in constructor or componentDidMount() (componentWillMount should not be used). the undefined result is caused by the async call. Looks like you have a lot of uncontrolled renders. try to add a shouldComponentUpdate function or convert your component in a PureComponent
Take a look at https://reactjs.org/docs/react-component.html
You have init the state with
state = {
data: [],
customColumns: []
}
Here this.state.data is empty array which did not have definition of
subscriptionRevisionDTOS that is why you are getting this.state.data.subscriptionRevisionDTOS undefined.
Meanwhile, your asyncaxios.get call is completed and this.state.data is updated with subscriptionRevisionDTOS.
As soon as state is updated render() called again and you are getting the proper value of this.state.data.subscriptionRevisionDTOS.
So below line will surely work.
state = {
data:{subscriptionRevisionDTOS:[]},
customColumns: []
}
export default class EditRevision extends Component {
state = {
data:{subscriptionRevisionDTOS:[]},
customColumns: []
}
componentDidMount = () => {
axios.get('http://localhost:8080/lagbevakning/revision/subscriptions?id=' +
(this.props.match.params.id)).then(response => {
this.setState({
data: response.data,
loading: false
})
})
render() {
/* THIS IS THE CONSOLE.LOG() I AM REFERRING TO */
console.log(this.state.data.subscriptionRevisionDTOS)
return (
<div></div>
)
}
see this it should be like this

Component not refreshing on firebase change

I have this code:
export default class MainStudentPage extends React.Component {
constructor(props) {
super(props);
this.state = {user: {nickname: '', friends: {accepted: [], invites: [], all: []}}};
}
componentWillMount() {
const {uid} = firebase.auth().currentUser;
firebase.database().ref('Users').child(uid).on('value', (r, e) => {
if (e) {
console.log(e);
return null;
}
const user = r.val();
this.setState({user: user});
});
}
render() {
const {user} = this.state;
return (
<LevelSelectComponent user={user}/>
</div>
);
}
}
And this is the child:
export default class LevelSelectComponent extends React.Component {
returnSelect = (user) => {
const lvls = [{
db: 'Podstawówka',
text: 'PODSTAWOWA'
}, {
db: 'Gimnazjum',
text: 'GIMNAZJALNA'
}, {
db: 'Liceum',
text: 'ŚREDNIA'
}];
let options = [];
if (!user.level) {
options.push(<option selected={true} value={null}>WYBIERZ POZIOM</option>)
}
options = options.concat(lvls.map((lvl, i) => {
return (
<option key={i} value={lvl.db}>{`SZKOŁA ${lvl.text}`}</option>
)
}));
return (
<select defaultValue={user.level}>
{options.map(opt => opt)}
</select>
)
};
render() {
const {user} = this.props;
return (
this.returnSelect(user)
);
}
}
So what I want is to refresh the default selected value to match the value in the database. I am listening to the firebase realtime database for changes. Every time I refresh the page, the defaultValue changes, as expected, but this doesn't do it in real time. It even logs the new value, but it doesn't rerender it. What am I missing?
Ok. All I had to do was change defaultValue to value
componentWillMount() this should not be the method where you use AJAX requests
instead, user componentDidMount().
Further:
componentWillMount() will only be invoked once, before the first render() for your component, thus it will not trigger a re-render for it, you should subscribe to your firebase realtime events in componentDidMount().

Update React state asynchronously from multiple sources

I'm trying to use immutability-helper to update my React state asynchronously from multiple sources (API calls). However, it seems to me that this is not the way to go, since state always gets updated with values from a single source only. Can someone explain me why is this the case and how to properly handle updates of my state?
import React from 'react';
import update from 'immutability-helper';
class App extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
updateInterval: 60, // in seconds
apiEndpoints: [
'/stats',
'/common/stats',
'/personal/stats',
'/general_info',
],
items: [
{ itemName: 'One', apiUrl: 'url1', stats: {} },
{ itemName: 'Two', apiUrl: 'url2', stats: {} },
],
};
this.fetchData = this.fetchData.bind(this);
}
componentDidMount() {
this.fetchData();
setInterval(() => this.fetchData(), this.state.updateInterval * 1000);
}
fetchData() {
this.state.apiEndpoints.forEach(endpoint => {
this.state.items.forEach((item, i) => {
fetch(`${item.apiUrl}${endpoint}`)
.then(r => r.json())
.then(res => {
// response from each endpoint contains different keys.
// assign all keys to stats object and set default 0 to
// those which don't exist in response
const stats = {
statsFromFirstEndpoint: res.first_endpoint ? res.first_endpoint : 0,
statsFromSecondEndpoint: res.second_endpoint ? res.second_endpoint : 0,
statsFromThirdEndpoint: res.third_endpoint ? res.third_endpoint : 0,
};
this.setState(update(this.state, {
items: { [i]: { $merge: { stats } } }
}));
})
.catch(e => { /* log error */ });
});
});
}
render() {
return (
<div className="App">
Hiya!
</div>
);
}
}
export default App;
You should use the prevState argument in setState to make sure it always use the latest state:
this.setState(prevState =>
update(prevState, {
items: { [i]: { $merge: { stats } } },
}));
Alternatively, map your requests to an array of promises then setState when all of them resolved:
const promises = this.state.apiEndpoints.map(endPoint =>
Promise.all(this.state.items.map((item, i) =>
fetch(), // add ur fetch code
)));
Promise.all(promises).then(res => this.setState( /* update state */ ));

React Native state isn't changing

I'm making a Ajax request to a Json file that return some movies.
state = { movies: [] };
componentWillMount()
{
this.getMovies();
}
/*
Make an ajax call and put the results in the movies array
*/
getMovies()
{
axios.get('https://pastebin.com/raw/FF6Vec6B')
.then(response => this.setState({ movies: response.data }));
}
/*
Render every movie as a button
*/
renderMovies()
{
const { navigate } = this.props.navigation;
return this.state.movies.map(movie =>
<ListItem key={ movie.title }
title={ movie.title }
icon={{ name: 'home' }}
onPress={() =>
navigate('Details', { title: movie.title, release: movie.releaseYear })
}
/>
);
}
render() {
return(
<List>
{ this.renderMovies() }
</List>
);
}
The error I get is the following: this.state.map is not a function. This is because movies is still empty.
When I console.log response.data it returns all the rows from the JSON file. So the problem is most likely in this line:
.then(response => this.setState({ movies: response.data }));
Does someone know what's wrong?
You put initial state in the wrong place. Do this instead:
constructor(props) {
super(props);
this.state = { movies: [] };
}
From document:
In general, you should initialize state in the constructor, and then
call setState when you want to change it.
Update you ajax request as following:
/*
Make an ajax call and put the results in the movies array
*/
getMovies()
{
let self = this;
axios.get('https://pastebin.com/raw/FF6Vec6B')
.then(response => self.setState({ movies: response.data }));
}
Also, you can bind your function inside constructor as:
constructor(props){
super(props);
this.getMovies = this.getMovies.bind(this);
}

Categories

Resources