The state changed but the render method from react does not trigger - javascript

I am currently self teaching myself code and at the moment diving into React.js. I am building a simple app which displays beer information pulled through the punkAPI (https://punkapi.com/documentation/v2).
In my app I wanted to add some kind of "infinite" scroll which would pull more data from the API when the user reaches the bottom of the page.
At the moment, the handleScroll method works, and updates the state of the component but it does not trigger the render method and I was wondering why.
I realize that there are a lot of things wrong with my code and I plan to restructure it, add some booleans in this.state to check if there is still more data to load, to handle errors and to not trigger the handleScroll method so intensively.
However, I am still wondering why there is no rendering happening even though the state is updated.
class BeerList extends Component {
constructor() {
super()
this.state = {
loading: false,
beers: []
}
this.handleScroll = this.handleScroll.bind(this)
}
componentDidMount() {
window.addEventListener('scroll', this.handleScroll, true);
this.setState({
loading: true
})
fetch("https://api.punkapi.com/v2/beers?per_page=12")
.then(response => response.json())
.then(data => {
this.setState({
loading: false,
beers: data
})
})
}
handleScroll() {
const checkForNewBeers = () => {
let lastBeerCard = document.querySelector(".beer-list > div:last-child");
let lastBeerCardOffset = lastBeerCard.offsetTop + lastBeerCard.clientHeight;
let pageOffset = window.pageYOffset + window.innerHeight;
if (pageOffset <= lastBeerCardOffset - 10) {
return;
}
this.setState(prevState => {
const beers = prevState.beers;
const page = (prevState.beers.length / 12) + 1;
fetch(`https://api.punkapi.com/v2/beers?per_page=12&page=${page}`)
.then(response => response.json())
.then(data => {
for (let item of data) {
beers.push(item);
}
console.log({
beers: beers
});
return {
beers: beers
}
});
});
}
document.addEventListener("scroll", function (event) {
checkForNewBeers();
});
}
render() {
let beerCards = []
if (this.state.beers.length > 0) {
beerCards = this.state.beers.map(beer => {
return <BeerCard
key = {
beer.id
}
img = {
beer.image_url
}
title = {
beer.name
}
description = {
beer.description
}
/>
})
}
return ( <
div className = "container" >
<
div className = "row beer-list" > {
beerCards
} <
/div> < /
div >
)
}
}
export default BeerList
So BeerCards are correctly appended when the page is loading then when you scroll the console shows you that the state is updated (way too much but still). I would expect the page loading a shit ton of BeerCards but nothing is happening. Why is that?

Instead of returning an object from your async fetch, call setState inside the .then()
let beers = this.state.beers;
const page = (this.state.beers.length / 12) + 1;
fetch(`https://api.punkapi.com/v2/beers?per_page=12&page=${page}`)
.then(response => response.json())
.then(data => {
for (let item of data) {
beers.push(item);
}
this.setState({ beers });
});

As soon as you invoke fetch which is the last statement inside setState, an undefined would be returned. Try transforming the setState parameter to an async function:
this.setState(async prevState => {
const beers = prevState.beers;
const page = prevState.beers.length / 12 + 1;
let response = await fetch(
`https://api.punkapi.com/v2/beers?per_page=12&page=${page}`
);
response = await response.json();
for (const item of response) {
beers.push(item);
}
console.log({
beers: beers
});
return {
beers: beers
};
});

You are so close! By simply adding the keyword "async" in front of "prevState" in "this.setState" and "return await" in front of "fetch", the state of your app should be updated and trigger a rerender as expected.

Related

setState is not working on 2nd time calling it inside componentDIdMount

im a beginner, im trying to make a memory game
this component fetches data for an api
then trims it down with only that has image link
then on level one it should display 3 random image from fetch data
it always
displayedChars: [undefined, undefined, undefined]
constructor(props) {
super()
this.state = {
level: 1,
numImg: 1*3,
displayedChars: [],
chars: []
}
}
async componentDidMount() {
await this.loadData().then(data => {
this.setState({
chars: this.trimData(data)
});
});
await this.displayChars().then(data => {
console.log(data)
this.setState({
displayedChars: data
});
});
console.log(this.state);
}
async loadData() {
try {
const res = await fetch(`http://hp-api.herokuapp.com/api/characters`)
const characters = await res.json();
return characters
} catch(err) {
console.error(err)
}
}
trimData(characters) {
const listChars = []
characters.map(char => {
if(char.image !== "") {
listChars.push(char)
}
})
return listChars
}
displayChars() {
return (new Promise((resolve) => {
const list = []
for(let x=1; x<= this.state.numImg; x++) {
let randomNum = Math.floor(Math.random() * 24);
list.push(this.state.chars[randomNum]);
}
console.log(list)
resolve(list)
}))
}
in the this.displayChars()
console.log(data) works fine
but
this.setState({
displayedChars: data
});
then console.log(this.state)
OUTPUT: [undefined, undefined, undefined]
setState is async, so you cannot see the updated states immediately, that's why your console.log is [undefined, undefined, undefined]. You can assign variables to handle responses separately instead of using this.state.
The second concern is you shouldn't mix then and async/await. I'd prefer using async/await alone in your case.
async componentDidMount() {
const chars = await this.loadData();
const displayedChars = await this.displayChars();
this.setState({
displayedChars: displayedChars
chars: this.trimData(chars)
});
//states are not updated right away
//console.log(this.state);
//access via variable
console.log({ chars, displayedChars })
}

Store JSON in a variable in a REACT Helper component

I'm new in react and i'm trying to do a fetch and then store the data in a variable.
I don't understand why my SetMovieResponse is not working, I tried to stringify the JSON I'm sending but each time I look at my console the var is undefined.
The response is fully clear but when I assign and display the data, it's undefined
Here is my code :
import React, { setState } from 'react';
const API_KEY = process.env.REACT_APP_API_KEY;
const PERSON_KEY = process.env.REACT_APP_PERSON;
const MOVIE_KEY = process.env.REACT_APP_MOVIE;
class Helper extends React.Component {
constructor() {
super()
this.state = {
answer: "",
actorAnswer: [],
movieAnswer: {},
myInit: {method: "GET", mode: "cors"}
}
}
setActorResponse = (response) => {
this.setState({ actorAnswer: response});
}
setMovieResponse = (response) => {
console.log("get call ? ",response)
this.setState({ movieAnswer: JSON.stringify(response)});
console.log("get SET ? ",this.movieAnswer)
}
fetchPersonFunction = (randomActor) => {
fetch(`${PERSON_KEY}${randomActor}?api_key=${API_KEY}`, this.myInit)
.then(res => {
return res.json();
})
.then(response => {this.setActorResponse(response.total)})
return (this.actorAnswer)
}
fetchMovieFunction = (randomMovie) => {
fetch(`${MOVIE_KEY}${randomMovie}?api_key=${API_KEY}`, this.myInit)
.then(res => {
return res.json();
})
.then(response => {this.setMovieResponse(response)
console.log("good response = ", response)
})
console.log("END res = ", this.movieAnswer);
return (this.movieAnswer)
}
}
export default Helper
So the console.log("get call ? ",response) Always display the JSON but the console.log("get SET ? ",this.movieAnswer)
I'm trying to call those function in a Game.js file like that :
const Help = new Helper();
console.log("MOVIE res = ",Help.fetchMovieFunction(randomMovie));
Who is just under and suppose to set movieAnswer is always undefined.
Is there any reason why my movieAnswer stay undefined when using setState ?
Thank's for you're help
You are accessing the state wrong. Your component state is accessible inside this.state property. ie, The value movieAnswer is available inside the this.state property.
Simply change all your state items to below,
console.log("get SET ? ",this.state.movieAnswer)
also change this.actorAnswer to,
this.state.actorAnswer
Also I don't think you need those return statements at the end of fetchPersonFunction and fetchMovieFunction functions.

Why is React not rendering my map function?

I have a map function to a state that I'm sure it is not empty because its states have been logged.
When i load the page it shows the div "did Load", but thats about it. Console also does not register he "running" log.
This is for CS50W course, so I am using in-browser Babel transformer. Someone suggested i completly switched my approach to compiling the code but that would mean learing and making a whole new setup, so I was hoping that is not the reason this is not working.
class Posts extends React.Component{
constructor(props){
super(props)
this.state = {
aposts: [],
isLoaded: false
}
}
componentDidMount() {
console.log('running go fetch');
let currentList = [];
fetch('/posts')
.then(response => response.json())
.then(posts => {
for (var i = 0; i < posts.length; i++) {
currentList.push(posts[i])
console.log(currentList[i])
}
});
console.log(currentList)
this.setState({ aposts: currentList, isLoaded: true }, () => {
console.log(this.state.aposts, 'aposts');
});
}
render(){
var isLoaded = this.state.isLoaded
if (isLoaded = true){
return (
<div>
<div>did load</div>
{this.state.aposts.map(function(apost) {
console.log("running")
return (<li key = {apost.id}> {apost.id} </li>)
})}
</div>
);
}
else{
<div>No load</div>
}
}
componentDidMount() {
console.log('running go fetch');
let currentList = [];
fetch('/posts')
.then(response => response.json())
.then(posts => {
for (var i = 0; i < posts.length; i++) {
currentList.push(posts[i])
console.log(currentList[i])
}
console.log(currentList)
this.setState({ aposts: currentList, isLoaded: true }, () => {
console.log(this.state.aposts, 'aposts');
});
});
When making api call it take some time so JS just run that work on some other process and your main process keep working.
Read about sync/asyn
and when you use .then it was when JS complete api call and get the response from server it should run anything which is inside .then
So you have to set state in .then so it will set value for state after it fetch values
You said you have to setup and learn to do setup. Why are you not using
[CRA][1] it will setup everything for you.

How can I wait until the functions finish in Reactjs?

Hi I am new to reactjs and I am trying to build button with a function doing some calculation by Reactjs. The logic is, first I will get two lists from database by two functions. After these 2 functions return results and setState, the calculate function will continue and do its job. But somehow the state is not being updated and it will crash. How can I secure the state is being updated before to the calculate? Thanks a lot!
Code:
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
dividendList : [],
divisorList : [],
};
}
getDividend(){
var self = this;
axios.post(SERVER_NAME + 'api/getDividend', {})
.then(function(response){
let results = response.data;
console.log(results)
self.setState({ dividendList : results.data})
})
.catch(function(err){
console.log(err)
});
}
getDivisor(){
var self = this;
axios.post(SERVER_NAME + 'api/getDivisor', {})
.then(function(response){
let results = response.data;
console.log(results)
self.setState({ divisorList : results.data})
})
.catch(function(err){
console.log(err)
});
}
doCal = () => {
var self = this;
self.getDividend();
self.getDivisor();
const { dividendList , divisorList} = self.state;
# then will loop the list and do math
# but since the state is not update, both lists are empty []
}
Tried Promise;
getDivisor(){
var self = this;
return new Promise((resolve, reject) => {
axios.post(SERVER_NAME + 'api/draw/getDivisor', {})
.then(function(response){
resolve(response)
})
.catch(function(err){
resolve();
});
})
}
I think the issue here is self.getDividend(); and self.getDivisor(); are async operations. They will take some time to complete. By the time you hit the next line const { dividendList , divisorList} = self.state;, these operations are not complete and you will end up getting empty lists.
One way to address this is using moving your doCal function logic after getDividend and getDivisor are completed. You can also execute these in parallel instead of in a sequence. I used async format instead of .then(). It is just a sysntatic sugar. You can achieve the same using .then() if you prefer that way
async function doCalc() {
const prom1 = axios.get('https://..dividentList');
const prom2 = axios.get('https://..divisorList');
const results = await Promise.all([ prom1, prom2]); // wait for both promise to complete
// look inside results to get your data and set the state
// continue doCal logic
}
Using .then()
request1('/dividentList')
.then((res) => {
//setState for divident
return request2('/divisorList'); // this will return a promise to chain on
})
.then((res) => {
setState for divisor
return Promise.resolve('Success') // we send back a resolved promise to continue chaining
})
.then(() => {
doCalc logic
})
.catch((err) => {
console.log('something went wrong');
});
I looked at your code and thought it should be changed like this to be correct.
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
dividendList: [],
divisorList: [],
};
}
componentDidMount() {
// the API just need be called once, so put here
this.getDividend()
this.getDivisor()
}
componentDidUpdate(_, prevState) {
const { dividendList , divisorList } = this.state;
// Ensure that the answer is only calculated once
// the answer is only be calculated while the two list data are obtained
if (
prevState.divisorList.length === 0 &&
prevState.dividendList.length === 0 &&
divisorList.length > 0 &&
dividendList.length > 0
) {
doCal()
}
}
getDividend(){
var self = this;
axios.post(SERVER_NAME + 'api/getDividend', {})
.then(function(response){
let results = response.data;
console.log(results)
self.setState({ dividendList : results.data})
})
.catch(function(err){
console.log(err)
});
}
getDivisor(){
var self = this;
axios.post(SERVER_NAME + 'api/getDivisor', {})
.then(function(response){
let results = response.data;
console.log(results)
self.setState({ divisorList : results.data})
})
.catch(function(err){
console.log(err)
});
}
doCal = () => {
const { dividendList , divisorList } = this.state;
# then will loop the list and do math
# but since the state is not update, both lists are empty []
this.setState({ answer: 'xxx' })
}
render() {
const { dividendList, divisorList, answer } = this.state
if (dividendList.length === 0 && divisorList.length === 0) {
return <div>Loading...</div>
}
if (!answer) {
return <div>Error</div>
}
return <div>{answer}</div>
}
}
The following are just some suggestions to make the code easier to read,
you can use arrow function so that you don't need to write self.setState({...})
getDividend = () => {
axios.post(SERVER_NAME + 'api/getDivisor', {})
.then((response) => {
let results = response.data;
console.log(results)
this.setState({ divisorList : results.data})
})
.catch((err) => {
console.log(err)
});
}
and you can also use async/await instead of promise.then
getDividend = async () => {
const response = await axios.post(SERVER_NAME + 'api/getDivisor', {})
let results = response.data;
console.log(results)
this.setState({ divisorList : results.data})
}
Set 'dividendList' and 'divisorList' equals to 'null' by default. Then, when a function that uses those lists is called, make a if statement to verify if those states goes for false (if they are still null) then return inside the function, if not, it should not crash anything.

setState while looping through an array of props - React ComponentDidUpdate

I am working on a project and needed to setState after componentDidMount.(The props am expecting in the child component are derived at mount. Hence i can only setState after)
Only option i was able to come up with was, using componentDidUpdate.
The props parent component is an array derived from an axios fetched data.
The goal here is to loop though the array and fetch data for each from the URL showing in the code below to then setState of the child component.
Trying what i normally do, I could not stop the infinite loop fired at componentDidUpdate.
Here is my code.
Parent
render(){
return (
<div className="App">
<EachCountry countryList= {this.state.CountriesList}/>
</div>
Child component
async componentDidUpdate(prevProps, prevState, snapshot){
if(this.state.countryList.length < this.props.countryList.length){
await this.props.countryList.map(m=>{
axios ({
method:"get",
url:`/countryupdate/${m}`
}).then(res=>{
console.log(res.data)
this.setState(crntState=>({
countryList:[...crntState.countryList, res.data]
}))
})
})
}
}
console log works perfectly fine. But when i tried to setState, i run into infinite loop with something like 5000 plus error messages.
And my other trick was
async componentDidUpdate(prevProps, prevState, snapshot){
if(this.state.countryList.length < this.props.countryList.length){
await this.props.countryList.map(m=>{
var newdata = axios ({
method:"get",
url:`/countryupdate/${m}`
})
console.log(newdata)
this.setState(crntState=>({
countryList:[...crntState.countryList, newdata.data]
}))
})
}
}
And this one returns promises and not the needed data.
Help Fam
What am i missing?
Your issue is likely caused by derived state: state that is dependent on props and is an anti pattern in react:
https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#when-to-use-derived-state
See below on a plausible workaround, though its recommended you restructure your data flow.
Try something like this to first only send 1 update to state:
async componentDidMount(){
//variable to store new data
const allNewData = [];
//an async data fetcher
const getNewData = async(m) => {
let newData = await axios({
method: "get",
url: `/countryupdate/${m}`
})
allNewData.push(newData.data);
}
//an async for loop
async function updateData() {
for (const m of countryList) {
await getNewData(m);
}
this.setState(crntState => ({
countryList: [...crntState.countryList, ...allNewData]
}))
}
await updateData();
}
If the above doesn't work (which it might not), then use getDerivedStateFromProps instead of componentDidMount and replace setState with a return obj
static getDerivedStateFromProps(props, state) {
if (this.state.countryList.length < this.props.countryList.length) {
...
return {
countryList: [...state.countryList, ...allNewData]
};
}
let newState = await updateData();
return newState;
}
If that doesn't work, then revert back to componentDidMount and use shouldComponentUpdate for the conditional
shouldComponentUpdate(nextProps, nextState) {
return this.state.countryList.length != nextState.countryList.length;
}
Incase I didn't get the syntax just right look at this code snippet
function mockAxios(m) {
return new Promise(function(resolve, reject) {
setTimeout(() => resolve({
data: `${m}'s data`
}), 1000)
});
}
function setState(arr) {
console.log(arr);
console.log("state has been set")
}
const countryList = ["A", "B", "C", "D", "E"];
///////////////////////////////////////////////
async function componentDidMount() {
const allNewData = [];
async function getNewData(m) {
let newData = await mockAxios(m);
allNewData.push(newData.data);
}
async function updateData() {
for (const m of countryList) {
await getNewData(m);
console.log("fetching data...")
}
setState(allNewData);
}
await updateData();
}
componentDidMount();

Categories