How to fix recursively updating state? - reactjs

I am bulding an app using newsapi. i am facing two issue on my state. i fetch data using api and assign it to my state. and use it in my view.
Issue no 1
My view gets rendered before my app receives the data.
Issue no 2
When I try to update my state after a new fetch. it recursively updates the set of data again and again.
import React, {Component} from 'react';
import NewsComponent from './NewsComponent/NewsComponent'
class News extends Component {
state = {
displayStatus: false,
newsItems: []
};
toogleDisplayHandler = () => {
if(this.state.displayStatus===true){
this.setState({displayStatus:false})
}
else{
this.setState({displayStatus:true})
}
}
render(){
const NewsAPI = require('newsapi');
const newsapi = new NewsAPI('d6da863f882e4a1a89c5152bd3692fb6');
//console.log(this.props.keyword);
newsapi.v2.topHeadlines({
sources: 'bbc-news,abc-news',
q: this.props.keyword
}).then(response => {
//console.log(response)
response.articles.map(article => {
//console.log(article);
return(
//console.log(this.state.newsItems)
this.setState({
newsItems: [...this.state.newsItems, article],
})
//this.state.newsItems.push(article)
)
});
});
let Article = null;
Article = (
<div>
{
this.state.newsItems.map((news, index) => {
return (
<NewsComponent key={index}
title={news.title}
url={news.url}
description={news.description}
author={news.author}
publish={news.publishedAt}
image={news.urlToImage}
/>
)
})
}
</div>
)
return (
<div className="App">
{Article}
<button onClick={this.toogleDisplayHandler}>
{this.state.displayStatus === true ? "Hide Article" : "Display Articles"}
</button>
</div>
)
}
}
export default News;
Please help me to resolve this issue.

You should never setState in render as that would cause an infinite loop. Do it in componentDidMount or the constructor.
I would also recommend not using map for simply iterating over a list. Array.map is a function that is useful for returning an array that is constructed by iterating over another array. If you want to run some code for each element of an array use Array.forEach instead.
Like this:
import React, { Component } from "react";
import NewsComponent from "./NewsComponent/NewsComponent";
class News extends Component {
state = {
displayStatus: false,
newsItems: []
};
toogleDisplayHandler = () => {
if (this.state.displayStatus === true) {
this.setState({ displayStatus: false });
} else {
this.setState({ displayStatus: true });
}
};
componentDidMount = () => {
const NewsAPI = require("newsapi");
const newsapi = new NewsAPI("d6da863f882e4a1a89c5152bd3692fb6");
newsapi.v2
.topHeadlines({
sources: "bbc-news,abc-news",
q: this.props.keyword
})
.then(response => {
response.articles.forEach(article => {
this.setState({
newsItems: [...this.state.newsItems, article]
});
});
});
};
render() {
let Article = null;
Article = (
<div>
{this.state.newsItems.map((news, index) => {
return (
<NewsComponent
key={index}
title={news.title}
url={news.url}
description={news.description}
author={news.author}
publish={news.publishedAt}
image={news.urlToImage}
/>
);
})}
</div>
);
return (
<div className="App">
{Article}
<button onClick={this.toogleDisplayHandler}>
{this.state.displayStatus === true
? "Hide Article"
: "Display Articles"}
</button>
</div>
);
}
}
export default News;

1) You can add a check either your state has the data which you want to show on screen to render the view.
2) Please use ComponentDidMount React life cycle function to fetch data from an external source and update this data in the state. In the Render method, it will keep calling it recursively.

Related

React state not updating accordingly

I have a search component where the search result updates according to the search input, where if there is data returned from the API, it is rendered as a book grid, if there is no data, a message is displayed, and if the search input is empty, nothing is rendered.
My problem is that when query state updates the searchResult state does update but when I delete the search input so fast (make the search input empty), query becomes updates as an empty string but searchResult does not update according to it. What could be the problem?
Here is the code to the search component: (Note: I tried the componentDidUpdate() method and the setState() callback function but nothing worked)
import React, { Component } from "react";
// import "React Router" components
import { Link } from "react-router-dom";
// import custom components
import Book from "./Book";
// import required API
import * as BooksAPI from "../BooksAPI";
export default class BookSearch extends Component {
state = {
query: "",
searchResult: [],
};
handleInputChange = (query) => {
this.setState(
{
query: query.trim(),
},
() => {
if (query) {
console.log(query);
BooksAPI.search(query).then((books) => {
this.setState({ searchResult: books });
});
} else {
this.setState({ searchResult: [] });
}
}
);
};
// componentDidUpdate(currentProps, currentState) {
// if (currentState.query !== this.state.query && this.state.query) {
// BooksAPI.search(this.state.query).then((books) => {
// this.setState({ searchResult: books });
// });
// } else if (currentState.query !== this.state.query && !this.state.query) {
// this.setState({ searchResult: [] });
// }
// }
render() {
const { query, searchResult } = this.state;
const { updateBookShelves } = this.props;
return (
<div className="search-books">
<div className="search-books-bar">
<Link to="/" className="close-search">
Close
</Link>
<div className="search-books-input-wrapper">
<input
type="text"
placeholder="Search by title or author"
value={query}
onChange={(event) => this.handleInputChange(event.target.value)}
/>
</div>
</div>
<div className="search-books-results">
<ol className="books-grid">
{ searchResult.error ?
<p>No results matching your search</p>
: searchResult.map((book) => (
<Book
key={book.id}
book={book}
updateBookShelves={updateBookShelves}
/>
))
)
) )}
</ol>
</div>
</div>
);
}
}
I am not 100% sure about this solution since it is using setState inside callback of other setState but you can give it a try.
I think you can probably need to use setTimeout before calling api for data and before checking if query exist or not we can set timeout to null so it will not call unwanted api calls.
handleInputChange = query => {
this.setState(
{
query: query.trim()
},
() => {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
if (query) {
this.timeout = setTimeout(() => {
BooksAPI.search(query).then(books => {
this.setState({ searchResult: books });
});
}, 800);
} else {
this.setState({ searchResult: [] });
}
}
);
};

if else statement not working in react component

I am trying to implement a condition in my react component . When the user triggers the onClick the state updates allStakes creating one array of 4 values. The problem is that I do not want the user to input more than 4 values so tried to give the limit by doing an if else statement. I tried to add a console.log in both statements.The weird fact is that setState get updated but the csonole.log is never displayed.The component keeps rendering all the values that I insert even if the array is longer than 4. Thanks in advance
import React, { Component } from 'react';
import Stake from './stake';
class FetchRandomBet extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
bet: null,
value: this.props.value,
allStakes: []
};
}
async componentDidMount() {
const url = "http://localhost:4000/";
const response = await fetch(url);
const data = await response.json();
this.setState({
loading: false,
bet: data.bets,
});
}
render() {
const { valueProp: value } = this.props;
const { bet, loading } = this.state;
const { allStakes } = this.state;
if (loading) {
return <div>loading..</div>;
}
if (!bet) {
return <div>did not get data</div>;
}
return (
< div >
{
loading || !bet ? (
<div>loading..</div>
) : value === 0 ? (
<div className="bet-list">
<ol>
<p>NAME</p>
{
bet.map(post => (
<li key={post.id}>
{post.name}
</li>
))
}
</ol>
<ul>
<p>ODDS</p>
{
bet.map(post => (
<li key={post.id}>
{post.odds[4].oddsDecimal}
<div className="stake-margin">
<Stake
onClick={(newStake) => {
if (allStakes.length <= 3) {
this.setState({ allStakes: [allStakes, ...newStake] })
console.log('stop')
} else if (allStakes.length == 4) {
console.log('more than 3')
}
}}
/>
</div>
</li>
))
}
</ul>
</div>
May be it happens because of incorrect array destructuring. Try to change this code:
this.setState({ allStakes: [allStakes, ...newStake] })
by the next one:
this.setState({ allStakes: [newStake, ...allStakes] })
Your state belongs to your FetchRandomBet component and you are trying to update that from your imported component. There are 2 solutions to that.
1> Wrap your Stake component to a separate component with onClick handler something like this
<div onClick={(newStake) => {
if (allStakes.length <= 3) {
this.setState({
allStakes: [allStakes, ...newStake
]
})
console.log('stop')
} else if (allStakes.length == 4) {
console.log('more than 3')
}
}}><Stake /></div>
Or
2> Pass the state as a prop to the Stake component which will be responsible to update the state for FetchRandomBet. something like this
<Stake parentState={this}/>
And inside the Stake component change the parentState on click of wherever you want.
I solved the problem. I transfered the onClick method in stake component and I handled the upload of the common array with an array useState. I add the value to newStake and when I click ok I retrieve newStake and spread it into a new array and then I check that array. If there is a value should not keep adding otherwise it can add values. It works fine. Thanks anyway
import React, { useState } from 'react';
import CurrencyInput from 'react-currency-input-field';
function Stake(props) {
const [newStake, setStake] = useState(null);
const [allStakes, setStakes] = useState(null);
const changeStake = (e) => {
setStake([e.target.value])
}
const mySubmit = () => {
if (!allStakes) {
setStakes([...newStake, allStakes])
props.onClick(newStake);
} else if (allStakes) {
console.log('stop')
}
}
return (
<>
<CurrencyInput
onChange={changeStake}
style={{
marginLeft: "40px",
width: "50px"
}}
placeholder="Stake"
decimalScale={2}
prefix="£"
/>
<button onClick={mySubmit}>yes</button>
<button>no</button>
{newStake}
</>
);
}
export default Stake;

TypeError: Cannot read property 'map' of undefined When trying to map multiple arrays

I am trying to map multiple arrays at the same time and im not sure if this is how you do it. I am getting the error
TypeError: Cannot read property 'map' of undefined
When trying the following code
import React, { Component } from 'react';
import Axios from 'axios';
import NavBar from '../header-footer/nav-bar'
import Featured from './FeaturedMealplan'
import RecipeItem from './RecipeItem'
export default class MealPlanDetail extends Component {
constructor(props) {
super(props);
this.state = {
currentId: this.props.match.params.slug,
mealplanItem: {}, // Full mealplan
mealplanRecipes: [], // Contains recipe names and difficulty.
}
}
getMealplanItem() {
Axios.get(`http://localhost:5000/get-mealplan/${this.state.currentId}`
).then(response => {
console.log("response", response)
this.setState({
mealplanItem: response.data.mealplan,
mealplanRecipes: this.state.mealplanRecipes.concat(response.data.mealplan["recipes"]),
mealplanIngredients: this.state.mealplanIngredients.concat(response.data.mealplan["recipe_info"]),
recipeItem: response.data.mealplan.recipes
})
}).catch(error => {
console.log("mealplan-detail GET Error ", error)
})
}
componentDidMount() {
this.getMealplanItem();
}
render() {
const renderRecipe = this.state.recipes.map((recipe, idx) => {
return (
<div key={idx}>
<h1>{recipe.recipe_name}</h1>
<h2>Recipe Difficulty: <span>{recipe.recipe_dificulty}</span></h2>
<div>
<RecipeItem recipeItem={this.state.recipeItem} />
</div>
</div>
)
})
return (
<div>
<NavBar/>
<Featured/>
{renderRecipe}
</div>
)
}
}
Data that is given: https://pastebin.com/uYUuRY6U
I just need to be able to format it correctly which this is how I would like it formatted in the renderRecipe return. I am new to mapping and do not know if there is a way to fix or a better way.
Some issues in the code that we can improve on:
this.state.recipes seems to be undefined in your logic. Is it a typo?
I would suggest implementing renderRecipe as a function instead of a variable.
You would only hope to render renderRecipe when there is data, but when your component is being mounted, this.state.recipes is undefined. It would only have value when getMealplanItem gets a response and being defined in the callback. So you should check whether the value is defined before rendering.
Please refer to my comments in the code below.
import React, { Component } from "react";
import Axios from "axios";
import NavBar from "../header-footer/nav-bar";
import Featured from "./FeaturedMealplan";
import RecipeItem from "./RecipeItem";
export default class MealPlanDetail extends Component {
constructor(props) {
super(props);
this.state = {
// ... define `recipes` if that's what you want
};
}
getMealplanItem() {
Axios.get(`http://localhost:5000/get-mealplan/${this.state.currentId}`)
.then((response) => {
console.log("response", response);
// ... set state `recipes` here if that's what you want
this.setState({
mealplanItem: response.data.mealplan,
mealplanRecipes: this.state.mealplanRecipes.concat(
response.data.mealplan["recipes"]
),
mealplanIngredients: this.state.mealplanIngredients.concat(
response.data.mealplan["recipe_info"]
),
recipeItem: response.data.mealplan.recipes
});
})
.catch((error) => {
console.log("mealplan-detail GET Error ", error);
});
}
componentDidMount() {
this.getMealplanItem();
}
render() {
const renderRecipe = () => {
// change renderRecipe from a variable to a function
if (!this.state?.recipes) {
// check whether `recipes` is a defined value
return null;
}
return this.state.recipes.map((recipe, idx) => {
return (
<div key={idx}>
<h1>{recipe.recipe_name}</h1>
<h2>
Recipe Difficulty: <span>{recipe.recipe_dificulty}</span>
</h2>
<div>
<RecipeItem recipeItem={this.state.recipeItem} />
</div>
</div>
);
});
};
return (
<div>
<NavBar />
<Featured />
{renderRecipe()} // it's a function call now
</div>
);
}
}
There is never a this.state.recipes defined. Based on data type and comment
this.state = {
currentId: this.props.match.params.slug,
mealplanItem: {}, // Full mealplan
mealplanRecipes: [], // Contains recipe names and difficulty.
}
I will assume you meant for it to really be this.state.mealplanRecipes.
Your render then becomes
const renderRecipe = this.state.mealplanRecipes.map((recipe, idx) => {...
This can easily handle the initial render with an empty array.

Map doesn't work in render after fetch in React JS

I'm struggling with fetching data and render to the screen in React JS
class Home extends Component{
componentWillMount(){
foods=[];
fetch('http://192.249.19.243:0280/main/get_recipes')
.then(res => res.json())
.then(data => foodlist=data)
.then(
() => console.log("f:",foodlist),
)
.then(
() => {foodlist.map(item => foods.push({title:item, img:"http://192.249.19.243:0280/main/image/"+item}));
console.log("foods", foods);
this.render();
}
);
}
componentDidMount(){
}
render(){
console.log("render in!");
return (
<div>
<ul>
{
console.log(foods), // this works fine -> 4 elements
foods.length!=0 ?
foods.map(item=>
<Item
title={item.title}
img={item.img}/>
)
:
<p id="loadingMsg">Data Loading...</p>
}
</ul>
</div>
);
}
}
export default Home;
in the render(), I checked console.log(foods) print 4 elements,
but Nothing appears in the screen..
I don't know why.. Please help me..
In react: it is not you who manage the render. If you want to render an element you need to call this.setState with the data that changed. You can see my example:
class Home extends Component{
state = {
foods: []
}
componentWillMount(){
fetch('http://192.249.19.243:0280/main/get_recipes')
.then(res => res.json())
.then(data => foodlist=data)
.then(
() => console.log("f:",foodlist),
)
.then(
() => {
this.setState({foods: foodlist.map(item => ({title:item, img:"http://192.249.19.243:0280/main/image/"+item})));
}
);
}
componentDidMount(){
}
render(){
console.log("render in!");
return (
<div>
<ul>
{
this.state.foods.length!=0 ?
this.state.foods.map(item=>
<Item
title={item.title}
img={item.img}/>
)
:
<p id="loadingMsg">Data Loading...</p>
}
</ul>
</div>
);
}
}
export default Home;
It looks like you are relatively new to React. I spot quite a few errors with this.
Please read the docs on class based components carefully
I have tried to refactor it without context. Give it a bash
class Home extends Component {
//initialize state in the constructor for class based components
constructor(props) {
super(props);
//foods must be an empty array otherwise .length may fail
this.state = { foods: [] }
};
//once the component has mounted, call the method which will perform the fetch
componentDidMount() {
this.fetchFoodData();
}
//calls the endpoint which returns a promise. The promise will then set the components state, which will trigger a render
fetchFoodData = () => {
fetch('http://192.249.19.243:0280/main/get_recipes')
.then(res => {
const foodData = res.json();
//not sure what your body looks like, but foods should be an array containing your food objects
const foods = foodData.map(item => foods.push({ title: item, img: "http://192.249.19.243:0280/main/image/" + item}));
//calling setState will cause react to call the render method.
this.setState({ foods: foods })
}).catch(err => {
//handle errors here
console.log(err);
});
};
//React calls this method when props or state change for this component
render() {
return (
<div>
<ul>
{
foods.length != 0 ?
foods.map(item =>
<Item
title={item.title}
img={item.img} />
)
:
<p id="loadingMsg">Data Loading...</p>
}
</ul>
</div>
);
}
}
export default Home;
Thats not the correct way to handle data in a react component. You should maintain list of foods in component state. Code sandbox: https://codesandbox.io/s/falling-bush-b9b78
As an example
import React from "react";
export default class App extends React.Component {
constructor() {
super();
this.state = {
foods: []
};
}
componentDidMount() {
const fetchMock = url =>
new Promise(resolve => {
setTimeout(() => resolve(["Barley", "Chicken", "Oats"]), 2000);
});
fetchMock("http://192.249.19.243:0280/main/get_recipes").then(foods => {
this.setState({
foods
});
});
}
render() {
console.log("render in!");
const { foods } = this.state;
return (
<div>
<ul>
{foods.length !== 0 ? (
foods.map(food => <h1 key={food}>{food}</h1>)
) : (
<p id="loadingMsg">Data Loading...</p>
)}
</ul>
</div>
);
}
}

Pass state value to component

I am really new in React.js. I wanna pass a state (that i set from api data before) to a component so value of selectable list can dynamically fill from my api data. Here is my code for fetching data :
getListSiswa(){
fetch('http://localhost/assessment-app/adminpg/api/v1/Siswa/')
.then(posts => {
return posts.json();
}).then(data => {
let item = data.posts.map((itm) => {
return(
<div key={itm.siswa_id}>
<ListItem
value={itm.siswa_id}
primaryText={itm.nama}
/>
</div>
)
});
this.setState({item: item});
});
}
From that code, i set a state called item. And i want to pass this state to a component. Here is my code :
const ListSiswa = () => (
<SelectableList>
<Subheader>Daftar Siswa</Subheader>
{this.state.item}
</SelectableList>
);
But i get an error that say
TypeError: Cannot read property 'item' of undefined
I am sorry for my bad explanation. But if you get my point, i am really looking forward for your solution.
Here is my full code for additional info :
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {List, ListItem, makeSelectable} from 'material-ui/List';
import Subheader from 'material-ui/Subheader';
let SelectableList = makeSelectable(List);
function wrapState(ComposedComponent) {
return class SelectableList extends Component {
static propTypes = {
children: PropTypes.node.isRequired,
};
getListSiswa(){
fetch('http://localhost/assessment-app/adminpg/api/v1/Siswa/')
.then(posts => {
return posts.json();
}).then(data => {
let item = data.posts.map((itm) => {
return(
<div key={itm.siswa_id}>
<ListItem
value={itm.siswa_id}
primaryText={itm.nama}
/>
</div>
)
});
this.setState({item: item});
});
}
componentWillMount() {
this.setState({
selectedIndex: this.props.defaultValue,
});
this.getListSiswa();
}
handleRequestChange = (event, index) => {
this.setState({
selectedIndex: index,
});
};
render() {
console.log(this.state.item);
return (
<ComposedComponent
value={this.state.selectedIndex}
onChange={this.handleRequestChange}
>
{this.props.children}
</ComposedComponent>
);
}
};
}
SelectableList = wrapState(SelectableList);
const ListSiswa = () => (
<SelectableList>
<Subheader>Daftar Siswa</Subheader>
{this.state.item}
</SelectableList>
);
export default ListSiswa;
One way to do it is by having the state defined in the parent component instead and pass it down to the child via props:
let SelectableList = makeSelectable(List);
function wrapState(ComposedComponent) {
return class SelectableList extends Component {
static propTypes = {
children: PropTypes.node.isRequired,
};
componentWillMount() {
this.setState({
selectedIndex: this.props.defaultValue,
});
this.props.fetchItem();
}
handleRequestChange = (event, index) => {
this.setState({
selectedIndex: index,
});
};
render() {
console.log(this.state.item);
return (
<ComposedComponent
value={this.state.selectedIndex}
onChange={this.handleRequestChange}
>
{this.props.children}
{this.props.item}
</ComposedComponent>
);
}
};
}
SelectableList = wrapState(SelectableList);
class ListSiswa extends Component {
state = {
item: {}
}
getListSiswa(){
fetch('http://localhost/assessment-app/adminpg/api/v1/Siswa/')
.then(posts => {
return posts.json();
}).then(data => {
let item = data.posts.map((itm) => {
return(
<div key={itm.siswa_id}>
<ListItem
value={itm.siswa_id}
primaryText={itm.nama}
/>
</div>
)
});
this.setState({item: item});
});
}
render() {
return (
<SelectableList item={this.state.item} fetchItem={this.getListSiswa}>
<Subheader>Daftar Siswa</Subheader>
</SelectableList>
);
}
}
export default ListSiswa;
Notice that in wrapState now I'm accessing the state using this.props.item and this.props.fetchItem. This practice is also known as prop drilling in React and it will be an issue once your app scales and multiple nested components. For scaling up you might want to consider using Redux or the Context API. Hope that helps!
The error is in this component.
const ListSiswa = () => (
<SelectableList>
<Subheader>Daftar Siswa</Subheader>
{this.state.item}
</SelectableList>
);
This component is referred as Stateless Functional Components (Read)
It is simply a pure function which receives some data and returns the jsx.
you do not have the access this here.

Resources