React setState() after fetch not rerendering - javascript

I am fetching data in componentDidMount() (I am getting them in the form I want) and I want to save them in the component state with this.setState.
The state is not changing.
I console log that I am getting to the point where setState is called - there are conditions
I tried const that = this
The component is not re-rendering and state is not changing and I would like to know why.
My code:
export class Offers extends Component {
constructor(props) {
super(props);
this.renderOffer = this.renderOffer.bind(this);
this.state = {
...
};
}
componentWillMount() {
this.setState(() => ({
offer: {},
isLoading: true,
isMyOffer: false,
...
}));
}
componentDidMount() {
console.log('MOUNTED');
const { profile } = this.props;
if (profile) {
this.setState(() => ({
isLoading: false
}));
}
if (profile && profile._id) {
this.setState(() => ({
isMyOffer: true,
...
}));
fetch(`/api/offers-by/${profile._id}`,{
method: 'GET'
})
.then(response => response.json())
.then(offers => {
if(!offers || !offers.length) {
this.setState(() => ({
isLoading: false
})
);
} else {
console.log('ELSE', offers[0]._id); // getting proper data
console.log('THIS', this) // getting this object
const offerData = offers[0]
this.setState(() => ({
offer: offerData,
isLoading: false
})) // then
}}) // fetch
console.log('STATE', this.state)
}
console.log('STATE', this.state)
}

setState has a callback method as the second argument.You should use that after the initial setState.This works because setState itself is an asynchronous operation.The setState() method does not immediately update the state of the component but rather if there are multiple setStates, they will be batched together into one setState call.
this.setState(() => ({
isLoading: false
}),() =>{
/// You can call setState again here and again use callback and call fetch and invoke setState again..
});
Ideally you could refactor some of your setStates into a single setState call.Start with an empty object and add properties to your object based on conditons.
const updatedState ={}
if(loading){
updatedState.loading = false
}
if(profile &&..){
updatedState.someProperty = value.
}
this.setState(updatedObject,()=> {//code for fetch..
}) // Using the object form since you don't seem to be in need of previous State.

Related

React componentDidUpdate makes infinit API calls on state change

I'm working with Class Components for the first time, and I'm facing an infinite loop.
My init state is
this.state = {
selectedYear: null,
error: null,
isLoaded: false,
items: [],
};
}
I have a function for fetching data
fetchData = () =>
fetch(
`https://jsonmock.hackerrank.com/api/football_competitions?year=${this.state.selectedYear}`
)
.then((res) => res.json())
.then(
(result) => {
this.setState({
isLoaded: true,
items: result.data,
});
},
(error) => {
this.setState({
isLoaded: false,
error,
});
}
);
and my componentDidUpdate method
componentDidUpdate(prevState) {
if (prevState.selectedYear != this.state.selectedYear) {
this.fetchData();
console.log(this.state.items);
}
}
The problem is when I change my year in state, fetchData gets called an infinite number of times.
It's beginner question but I never worked with class components.
Thanks
As per the React documentation, the signature of componentDidUpdate method is
componentDidUpdate(prevProps, prevState, snapshot)
what you are doing in your code is checking this.state.selectedYear with prevProps.selectedYear (this will be most probably undefined)
Try below,
componentDidUpdate(_, prevState) {
if (prevState.selectedYear != this.state.selectedYear) {
this.fetchData();
console.log(this.state.items);
}
}

Cannot call the first element of a JSON object

I'm trying to access the first object from data[]. Then, grab the keys using Object.keys() but it gives me this error:
"TypeError: Cannot convert undefined or null to object".
I need the output to be an array of the keys.
import React, { Component } from 'react';
class CodecChart extends Component {
constructor(props) {
super(props);
this.state = {
post: [],
isLoaded: false,
}
}
componentDidMount() {
const url = 'https://jsonplaceholder.typicode.com/users';
fetch(url)
.then(result => result.json())
.then(post => {this.setState({ post: post })
})
}
render() {
const data = this.state.post;
// cannot reach the first object of data[]
var keys = Object.keys(data[0]);
return (
<div>
//output should be an array of the keys
<h5>{keys}</h5>
</div>
)
}
}
export default CodecChart;
The first time you try to access data[0], it's still empty:
this.state = {
post: [],
isLoaded: false,
}
and const data = this.state.post; means that data[0] is undefined.
it's only after the component is mounted, and the state is set correctly that data[0] is defined (or not, depending on what the API returns).
I found a way for it to work by adding another "then" so it can set the "keys" state right after the "posts" state was set. But I wonder if there is another way to make it more elegant. Thank you for trying to help.
constructor(props) {
super(props);
this.state = {
posts: [],
isLoaded: false,
keys: []
}
}
componentDidMount() {
const url = 'https://jsonplaceholder.typicode.com/users';
fetch(url)
.then(result => result.json())
.then(posts => {
this.setState({ posts: posts })
})
.then(_ => { this.setState({ keys: Object.keys(this.state.posts[0]) }) })
}
render() {
const keys = this.state.keys;
return (
<div>
<h5>{keys}</h5>
</div>
)
}

React componentDidMount fetch api

I am trying to fetch an api inside componentDidMount. The api result will be set to the component's state and the state mapped and passed to a children component.
If I fetch the api using the fetch method inside the componentDidMount everything works fine:
componentDidMount(){
fetch(apiToFetch)
.then((result) => result.json())
.then((result) => result.entries)
.then((entries) => this.setState({ ...this.state, entries }))
.catch((error) => console.log(error));
}
if I use fetch inside a method and then call this method inside componentDidMount nothing is rendered:
componentDidMount() {
this.fetchApiToEntries(GLOBAL_PORTFOLIO_COLLECTION_API);
}
fetchApiToEntries(apiToFetch) {
fetch(apiToFetch)
.then((result) => result.json())
.then((result) => result.entries)
.then((entries) => this.setState({ ...this.state, entries }))
.catch((error) => console.log(error));
}
I cannot understand what I am missing from the lifecycle.
Shouldn't react do the following?
Init the state
Render
Call componentDidMount
Rerender
Here is my initial component:
export default class Portfolio extends React.Component {
constructor(props) {
super(props);
this.state = {
entries: []
}
}
componentDidMount() {
this.fetchApiToEntries(GLOBAL_PORTFOLIO_COLLECTION_API);
}
fetchApiToEntries(apiToFetch) {
fetch(apiToFetch)
.then((result) => result.json())
.then((result) => result.entries)
.then((entries) => {
this.setState({
...this.state,
entries
})
})
.catch((error) => console.log(error));
}
render() {
return (
<Fade bottom>
<div className="Portfolio">
<div className="Portfolio__title"><h4 className="color--gradient text--spacing">WORKS</h4></div>
<OwlCarousel {...options}>
{this.state.entries.map((item) => (
<PortfolioElement item={item} />
))}
</OwlCarousel>
<AnchorLink href='#contact'><Button className="contact-button btn--gradient slider-button Services__button">Let's get in touch</Button></AnchorLink>
</div>
</Fade>
)
}
}
PortfolioElement is the actual component not being rendered.
Any advice?
Thank you.
Edit: both methods are not rerendering the component the right way (...something I didn't expect: I don't know why but if I call them twice in componentDidMount the component will render the right way). I think I am missing something in the state.
I have no error in the console and this is how I set my initial state:
this.state={entries:[]}
and this is what the actual entries looks like from the console:
entries:
[0:{credits: "..."
description: "..."
featuredImage: {path: "portfolio/01.jpg"}
firstImage: {path: "portfolio/firstimage.jpg"}
secondImage: []
slug: "..."
tasks: (5) [{…}, {…}, {…}, {…}, {…}]
title: "..."
_by: "5beae6553362340b040001ee"
_created: 1542123322
_id: "5beaef3a3362340bf70000b4"
_mby: "5beae6553362340b040001ee"
_modified: 1542149308
},
1:{...}
]
My state after the fetch is the same way.
UPDATE I figured out that the the problem is: when the state changes the component is not rerendering the child with the correct props. I called the API in an higher order component passed down the props and added a componentWillUpdate method forcing a state refresh that rerenders the component. Not the ideal solution but I am not figuring out other ways until now. Any advice?
Idk what your api response is but I tested your code with a fake API and changed
fetchApiToEntries(apiToFetch){}
to Arrow Function (Arrow Function)
fetchApiToEntries = (apiToFetch) => {}
and it's working fine.
Full Example:
export default class Portfolio extends React.Component {
constructor(props) {
super(props);
this.state = {
entries: []
}
}
componentDidMount() {
this.fetchApiToEntries('https://jsonplaceholder.typicode.com/posts');
}
fetchApiToEntries = (apiToFetch) => {
fetch(apiToFetch)
.then(result => result.json())
.then((entries) => {
this.setState({
...this.state,
entries
})
})
.catch((error) => console.log(error));
}
render() {
const {entries} = this.state;
console.log(entries);
return (
// Everything you want to render.
)
}
}
Hope it helps you.
do you need to bind fetchApiToEntries in the constructor or use fat arrows?
this.fetchApiToEntries = this.fetchApiToEntries.bind(this);
sorry I cant comment not enough rep

React conditional subset of state element

I have a set of results from an api, stored in the state as the array 'results'.
I want a second array, 'visible_results' in the state. This should be a subset of 'results'. I'm trying to do this like this:
export default class SearchScreen extends Component {
constructor(props) {
super(props);
this.state = {
results: null,
visible_results: null,
low_price: null,
high_price: null,
min_price: null,
max_price: null
};
}
componentDidMount() {
const apiUrl = 'foo';
fetch(apiUrl)
.then(response => response.json())
.then(response => {
this.setState({
results: response.results,
min_price: 1,
max_price: 100
});
this.setState({
low_price: this.state.min_price,
high_price: this.state.max_price
});
});
}
handleChange = event => {
const { name, value } = event.target;
this.setState({
[name]: +value
});
this.setState({
visible_results: this.state.results.reduce((items, x) => {
if (this.state.low_price <= x.price && x.price <= this.state.high_price)
items.push(x);
return items;
})
});
};
The handleChange is tied to two sliders, one setting low_price, and one setting high_price. The function should then generate a subset of results, based on the new value of low_price or high_price, and save it to the state as visible_results.
It doesn't work. There are no errors, but visible_results always remain 'null'. The sliders definitely work. I've tried replacing the if statement with if (1==1) to make sure that it wasn't just an if statement typo. It did the same thing.
A few things:
Setting the values to null makes your code more complicated, I would either add a useful default value, e.g. min_price: 0, max_price: Infinity or just don't initialize it.
this.setState is asynchronous! If you call setState twice, the first call will be deferred, so this.state isn't yet update in the second call, so e.g. this.state.low_price inside the second call in handleChange wasn't yet updated.
If you don't pass an initial value to reduce, it will take the arrays first element, which in your case is an object, calling push on this won't work. You probably want .reduce(fn, []), but in your case .filter is actually more appropriate.
visible_results doesn't have to be part of the state as it is determined by other state props, so just determine it on render
Code:
export default class SearchScreen extends Component {
constructor(props) {
super(props);
this.state = {
results: [],
low_price: 0,
high_price: Infinity,
min_price: 0,
max_price: Infinity
};
}
componentDidMount() {
const apiUrl = 'foo';
fetch(apiUrl)
.then(response => response.json())
.then(response => {
this.setState({
results: response.results,
min_price: 1,
max_price: 100,
min_price: 1,
high_price: 100,
});
});
}
handleChange = event => {
const { name, value } = event.target;
this.setState({
[name]: +value
});
}
render() {
const { results, low_price, high_price } = this.state;
const visibleResults = results.filter(item =>
low_price <= item.price && item.price <= high_price);
//...
}
}

Can't call setState (or forceUpdate) on an unmounted component. This is a no-op, but it indicates a memory leak in your application

Why am I getting this error?
Warning: Can't call setState (or forceUpdate) on an unmounted
component. This is a no-op, but it indicates a memory leak in your
application. To fix, cancel all subscriptions and asynchronous tasks
in the componentWillUnmount method.
postAction.js
export const getPosts = () => db.ref('posts').once('value');
components:
constructor(props) {
super(props);
this.state = { posts: null };
}
componentDidMount() {
getPosts()
.then(snapshot => {
const result = snapshot.val();
this.setState(() => ({ posts: result }));
})
.catch(error => {
console.error(error);
});
}
componentWillUnmount() {
this.setState({ posts: null });
}
render() {
return (
<div>
<PostList posts={this.state.posts} />
</div>
);
}
As others mentioned, the setState in componentWillUnmount is unnecessary, but it should not be causing the error you're seeing. Instead, the likely culprit for that is this code:
componentDidMount() {
getPosts()
.then(snapshot => {
const result = snapshot.val();
this.setState(() => ({ posts: result }));
})
.catch(error => {
console.error(error);
});
}
since getPosts() is asynchronous, it's possible that before it can resolve, the component has unmounted. You're not checking for this, and so the .then can end up running after the component has unmounted.
To handle that, you can set a flag in willUnmount, and check for that flag in the .then:
componentDidMount() {
getPosts()
.then(snapshot => {
if (this.isUnmounted) {
return;
}
const result = snapshot.val();
this.setState(() => ({ posts: result }));
})
.catch(error => {
console.error(error);
});
}
componentWillUnmount() {
this.isUnmounted = true;
}
React component's state is a local entity. Unmounted component dont have state, no need to do that. React is already telling you this is a no-op which means no-operation in tech speak. It means that you telling component to do something when its already destroyed.
https://reactjs.org/docs/react-component.html#componentwillunmount
You should not call setState() in componentWillUnmount() because the component will never be re-rendered. Once a component instance is unmounted, it will never be mounted again.
Remove this
componentWillUnmount() {
this.setState({ posts: null });
}
it's useless
From the doc:
You should not call setState() in componentWillUnmount() because the
component will never be re-rendered. Once a component instance is
unmounted, it will never be mounted again.
https://reactjs.org/docs/react-component.html#componentwillunmount
You can try this code:
constructor(props) {
super(props);
this.state = { posts: null };
}
_isMounted = false;
componentDidMount() {
this._isMounted = true;
getPosts()
.then(snapshot => {
const result = snapshot.val();
if(this._isMounted) {
this.setState(() => ({ posts: result }))
}
})
.catch(error => {
console.error(error);
});
}
componentWillUnmount() {
this._isMounted = false;
this.setState({ posts: null });
}
render() {
return (
<div>
<PostList posts={this.state.posts} />
</div>
);
}
By using _isMounted, setState is called only if the component is mounted. the answer simply does a check to see if the component is mounted before setting the state.

Categories

Resources