i want to cancel the load_items request on componentWillUnmount. I am not sure how to do it? could someone help me with this. Thanks.
What i have tried to do?
I have a parent component ViewItems and child component ItemsList. In the ViewItems component i retrieve the items list from the server using load_items method which in turn uses client.get_file method. and store those items list in state named "items". I call this load_items method in componenDidMount method.However doing so it sometimes displays items for previous item. This happens sometimes. I would like to have the proper item details in the state.
I think i have to cancel the load_items request before updating the state again.
Below is the code ,
class ViewItems extends React.PureComponent {
constructor(props) {
super(props);
this.default_root_item = {
name: 'Items',
index: 0,
children: [],
};
this.state = {
root_items: this.default_root_item,
items: [],
};
}
componentDidMount() {
this.load_items();
}
componentDidUpdate(PrevProps) {
if (PrevProps.item_id !== this.props.item_id) {
this.load_items();
}
}
componentWillUnmount() {
this.unlisten_path_change();
}
load_items = () => {
const file_name = 'file_name.json';
client.get_file(this.props.item_id, file_name, 'json')
.then((request) => {
const items = request.response;
this.setState({items: [this.default_root_item]});}
this.handle_path_change(this.props.location.pathname);})};
return (
<ChildComponent
on_close={this.handle_item_close}
root_item={this.state.root_item}/>)}
export default class ChildComponent extends React.PureComponent {
<Items
items={root_item}/>
function Items(props) {
return (
<ul className="Items_list">
<div className="items">
{props.items.map((item, index) => {
return (
<Item
key={index}
item={item}
/>
);
})}
</div>
</ul>
);
}
}
export function get_file(qrcode, file_name, response_type,on_progress, cancel) {
const local_override_defined = item_files[qrcode] && item_files[qrcode][file_name];
if (local_override_defined) {
const file = item_files[qrcode][file_name];
const reader = new FileReader();
return new Promise(resolve => {
if (response_type === 'blob') {
resolve({response: file});
} else {
reader.onload = () => {
if (response_type === 'json') {
resolve({response: JSON.parse(reader.result)});
} else {
resolve({response: reader.result});
}
};
reader.readAsText(file);
}
});
}
return new Promise((resolve, reject) => {
item_file_get_url(qrcode, file_name).then(({response}) => {
const request = new XMLHttpRequest();
request.addEventListener('progress', on_progress);
request.open('GET', response.url);
request.responseType = response_type;
send_request(request, undefined, cancel, response.url).then(resolve).catch(reject);
}).catch(reject);
});
}
Related
I have created three react components and I don't know why I am getting an infinite network request and this warning: index.js:1375 Warning: Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state.
in MenuCategory (at App.js:19)
in App (at src/index.js:5)
also a network request in MenuItems.js is getting called in a loop. I think it is due to setState but I don't know where is the error.
And here is my code :
import React from "react";
import MenuCategory from "./components/MenuCategory";
import MenuItems from "./components/MenuItems";
class App extends React.Component {
constructor(props) {
super(props);
this.state = { shortName: "" };
}
handleProps = ss => {
if (this.state.shortName === "") {
this.setState({ shortName: ss });
}
// console.log(ss, ".../PP");
};
render() {
return (
<div className="App">
<MenuCategory callback={this.handleProps} />
<MenuItems shortNameProp={this.state.shortName} />
</div>
);
}
}
export default App;
import React from "react";
class MenuCategory extends React.Component {
constructor(props) {
super(props);
this.state = { category: "", selectedCat: "" };
}
async UNSAFE_componentWillMount() {
const url = "http://stream-restaurant-menu-svc.herokuapp.com/category";
await fetch(url)
.then(data => data.json())
.then(element => {
this.setState({ category: element });
});
}
menuCat = () => {
let cat = this.state.category;
// console.log(cat, "...Cat", this.state.selectedCat, "...Cat");
if (this.state.selectedCat !== "") {
this.props.callback(this.state.selectedCat);
}
return cat.map(items => {
return (
<li
key={items.short_name}
onClick={() => this.setState({ selectedCat: items.short_name })}
>
{items.name}
</li>
);
});
};
render() {
return <div>{this.state.category.length > 0 ? this.menuCat() : null}</div>;
}
}
export default MenuCategory;
import React from "react";
class MenuItems extends React.Component {
constructor(props) {
super(props);
this.state = { catItems: "", items: "" };
}
renderItems = () => {
let shortName = this.props.shortNameProp;
if (shortName !== "") {
const url =
"https://stream-restaurant-menu-svc.herokuapp.com/item?category=" +
shortName;
fetch(url)
.then(data => data.json())
.then(element => {
this.setState({ items: element });
});
}
if (this.state.items !== "") {
let selectedMenu = this.state.items;
console.log(selectedMenu);
return selectedMenu.map(item => {
return <div key={item.name}> {item.name}</div>;
});
}
};
render() {
return <div>{this.renderItems()}</div>;
}
}
export default MenuItems;
Let's call App a parent and MenuCategory a child.
Let's denote a function call as the '->' sign.
There is an infinite loop formed like that:
child.render -> child.menuCat -> child.props.callback -> parent.handleProps -> parent.setState -> parent.render -> child.render.
The parent component Dashboard holds the state for every ListItem I add to my Watchlist. Unfortunately, every time I am adding an Item, it gets added to the DB, but only shows up when I refresh the browser.
class UserDashboard extends React.Component {
state = {
data: []
}
componentWillMount() {
authService.checkAuthentication(this.props);
}
isLoggedIn = () => {
return authService.authenticated()
}
getAllCoins = () => {
//fetches from backend API
}
addWishlist = () => {
this.getAllCoins()
.then(things => {
this.setState({
data: things
})
})
console.log("CHILD WAS CLICKED")
}
componentDidMount() {
this.getAllCoins()
.then(things => {
this.setState({
data: things
})
})
}
render() {
return (
<div className="dashboard">
<h1>HI, WELCOME TO USER DASHBOARD</h1>
<SearchBar
addWishlist={this.addWishlist}
/>
<UserWatchlist
data={this.state.data}
/>
</div>
);
}
}
The User Watchlist:
class UserWatchlist extends React.Component {
constructor(props) {
super(props)
}
// componentDidUpdate(prevProps) {
// if (this.props.data !== prevProps.data) {
// console.log("CURRENT", this.props.data)
// console.log("PREVs", prevProps.data)
// }
// }
render() {
return (
<div>
<h2>These are tssssyou are watching:</h2>
<ul className="coin-watchlist">
{
this.props.data.map((coin, idx) => {
return <ListItem key={idx}
coin={coin.ticker}
price={coin.price}
/>
})
}
</ul>
</div>
)
}
}
The search Bar that shows potential Items to watch over:
class SearchBar extends React.Component {
constructor(props) {
super(props)
this.state = {
coins: [],
searchValue: ""
}
}
searchHandler = e => {
e.preventDefault()
const value = e.target.value
this.setState({
searchValue: value
});
if (value === "") {
this.setState({
coins: []
})
} else {
this.getInfo()
}
}
getInfo = () => {
// Searches the API
}
addWishlist = () => {
this.props.addWishlist();
}
render() {
const {coins, searchValue} = this.state
return (
<div className="coin-search">
<form>
<input
type="text"
className="prompt"
placeholder="Search by ticker symbol"
value={searchValue}
onChange={this.searchHandler}
/>
</form>
<ul className="search-suggestions">
{
coins.filter(searchingFor(searchValue)).map( coin =>
<Currency
coin={coin}
addWishlist={this.addWishlist}
/>
)
}
</ul>
</div>
);
}
}
And the actual Currency that gets clicked to be added:
class Currency extends React.Component {
addToWatchlist = () => {
// POST to backend DB to save
};
fetch("/api/add-coin", settings)
.catch(err => {
return err
})
}
clickHandler = () => {
this.addToWatchlist()
this.props.addWishlist()
}
render() {
return(
<div className="search-results">
<li>
<h3> { this.props.coin.currency } </h3>
<button
className="add-to-list"
onClick={this.clickHandler}
>
+ to Watchlist
</button>
</li>
</div>
)
}
}
As you can see, I am sending props down all the way down to child. When I click the button to Add to Watchlist, I see the console.log message appear, saying "CHILD WAS CLICKED". I've even tried just calling the method to fetch from backend API again.
Also, in UserWatchlist, I've tried a componentDidUpdate, but both prevProps and this.props show the very same array of data. Somewhere in the chain, my data is getting lost.
This is also my first time posting a question here, so if it can be improved, I am happy to add extra details and contribute something to this community
You probably forgot to wait for addToWatchlist to complete:
addToWatchlist = () => {
// POST to backend DB to save
return fetch("/api/add-coin", settings)
.catch(err => {
return err
})
}
clickHandler = () => {
this.addToWatchlist().then(() => {
this.props.addWishlist()
})
}
I know the state is updating because 1. the 'Loading...' is going away, I can console log this.state.images to see the array. However when the state updates and the loading goes the searchbar shows up but the Card's within CardList do not.
They do show up when I search for a correct string, but not before.
If I pass this.state.images to CardList they show up perfectly. However when I move to the filteredImages they only show up when filtered.
Any ideas? Thanks in advance.
class App extends Component {
constructor() {
super();
this.state = {
images:[],
searchfield: ''
}
}
getLabels = (image) => {
const AuthKey = key.key;
const res = axios.post(`https://vision.googleapis.com/v1/images:annotate?key=${AuthKey}`, {
requests: [
{
image:{
source:{
imageUri: `http://storage.googleapis.com/${image}`
}
},
features:[
{
type:"LABEL_DETECTION",
maxResults:10
}
]
}
]
});
res.then(function (response) {
const results = response.data.responses[0].labelAnnotations;
const ex = results.map(result => {
return result.description;
});
return ex;
});
return res;
};
componentDidMount() {
imageFiles.imageFiles.forEach(img => {
this.getLabels(img).then(result => {
const results = result.data.responses[0].labelAnnotations;
const labels = results.map(result => {
return result.description;
});
//Add new values to the state
this.setState({images:[...this.state.images, {img, labels}]});
});
})
}
onSearchChange = (event) => {
this.setState({searchfield: event.target.value});
}
render() {
const filteredImages = this.state.images.filter(image => {
return image.labels.includes(this.state.searchfield.toLowerCase());
});
// Create an array of objects to store the image path and the labels detected from Google Vision
if (this.state.images.length === 0) {
return <h1>Loading...</h1>
} else {
return (
<Grid className="App">
<SearchBox searchChange={this.onSearchChange}/>
<CardList images={filteredImages} />
</Grid>
)}
}
}
export default App;
class App extends Component {
constructor() {
super();
this.state = {
images:[],
searchfield: '',
filteredImages:[]
}
}
getLabels = (image) => {
const AuthKey = key.key;
const res = axios.post(`https://vision.googleapis.com/v1/images:annotate?key=${AuthKey}`, {
requests: [
{
image:{
source:{
imageUri: `http://storage.googleapis.com/${image}`
}
},
features:[
{
type:"LABEL_DETECTION",
maxResults:10
}
]
}
]
});
res.then(function (response) {
const results = response.data.responses[0].labelAnnotations;
const ex = results.map(result => {
return result.description;
});
return ex;
});
return res;
};
componentDidMount() {
imageFiles.imageFiles.forEach(img => {
this.getLabels(img).then(result => {
const results = result.data.responses[0].labelAnnotations;
const labels = results.map(result => {
return result.description;
});
//Add new values to the state
this.setState({images:[...this.state.images, {img, labels}]});
this.setState({filteredImages:[...this.state.images, {img, labels}]});
});
})
}
onSearchChange = (event) => {
this.setState({searchfield: event.target.value});
let filteredImages = this.state.images.filter(image => {
return image.labels.includes(this.state.searchfield.toLowerCase());
});
this.setState({filteredImages});
}
render() {
// Create an array of objects to store the image path and the labels detected from Google Vision
if (this.state.images.length === 0) {
return <h1>Loading...</h1>
} else {
return (
<Grid className="App">
<SearchBox searchChange={this.onSearchChange}/>
<CardList images={this.state.filteredImages} />
</Grid>
)}
}
}
export default App;
I'm trying to implement a restaurant app where a user can add dishes to a menu. The menu will be displayed in a side bar. Dish information is provided through an API. I'm having issues with the API requests/promises. I'm storing a list of the dishes in DinnerModel. I'm making the requests to the API in DinnerModel.
When I add a dish to the menu by clicking the add button in IngredientsList, I get redirected to a screen that shows Sidebar. But in Sidebar, the dishes are NaN. The console.logs show that this.state.menu in Sidebar is actually a Promise, not an array. I'm having trouble understanding why this is and what to do about it.
Note that update in Sidebar is supposed to run modelInstance.getFullMenu() which returns an array. But instead, a promise is returned. Why? What can I do to fix this?
Here's my code:
Dinnermodel.js:
const DinnerModel = function () {
let numberOfGuests = 4;
let observers = [];
let selectedDishes = [];
// API Calls
this.getAllDishes = function (query, type) {
const url = 'https://spoonacular-recipe-food-nutrition-v1.p.mashape.com/recipes/search?query='+query+"&type="+type;
return fetch(url, httpOptions)
.then(processResponse)
.catch(handleError)
}
//function that returns a dish of specific ID
this.getDish = function (id) {
let url = "https://spoonacular-recipe-food-nutrition-v1.p.mashape.com/recipes/"+id+"/information";
return fetch(url, httpOptions)
.then(processResponse)
.catch(handleError)
}
// API Helper methods
const processResponse = function (response) {
if (response.ok) {
return response.json();
}
throw response;
}
this.addToMenu = function(id, type){
var newDish = this.getDish(id).then()
newDish.dishType = type;
selectedDishes.push(newDish);
notifyObservers();
}
//Returns all the dishes on the menu.
this.getFullMenu = function() {
return selectedDishes;
}
DishDetails.js:
class DishDetails extends Component {
constructor(props) {
super(props);
this.state = {
id: props.match.params.id,
status: "INITIAL",
type: props.match.params.type,
};
}
addToMenu (){
modelInstance.addToMenu(this.state.id, this.state.type);
this.props.history.push("/search/"+this.state.query+"/"+this.state.type);
}
componentDidMount = () => {
modelInstance.getDish(this.state.id)
.then(dish=> {
this.setState({
status:"LOADED",
ingredients: dish.extendedIngredients,
dishText: dish.winePairing.pairingText,
pricePerServing: dish.pricePerServing,
title: dish.title,
img: dish.image,
instructions: dish.instructions,
})
})
.catch(()=>{
this.setState({
status:"ERROR",
})
})
}
render() {
switch(this.state.status){
case "INITIAL":
return (
<p>Loading...</p>
);
case "ERROR":
return (
<p>An error has occurred, please refresh the page</p>
);
}
return (
<IngredientsList ingredients={this.state.ingredients} pricePerServing={this.state.pricePerServing} id={this.state.id} onButtonClick={() => this.addToMenu()}/>
<Sidebar />
);
}
}
export default withRouter(DishDetails);
Sidebar.js:
class Sidebar extends Component {
constructor(props) {
super(props)
// we put on state the properties we want to use and modify in the component
this.state = {
numberOfGuests: modelInstance.getNumberOfGuests(),
menu: modelInstance.getFullMenu(),
}
modelInstance.addObserver(this);
}
// this methods is called by React lifecycle when the
// component is actually shown to the user (mounted to DOM)
// that's a good place to setup model observer
componentDidMount() {
modelInstance.addObserver(this)
}
// this is called when component is removed from the DOM
// good place to remove observer
componentWillUnmount() {
modelInstance.removeObserver(this)
}
handleChangeGuests(event){
let noOfGuests = event.target.value;
modelInstance.setNumberOfGuests(noOfGuests);
}
// in our update function we modify the state which will
// cause the component to re-render
update() {
this.setState({
numberOfGuests: modelInstance.getNumberOfGuests(),
menu: modelInstance.getFullMenu(),
})
console.log("menu in Sidebar.js");
console.log(this.state.menu);
}
render() {
//console.log(this.state.menu);
let menu = this.state.menu.map((dish)=>
<div key={"menuitem-"+dish.id} className="menuitemwrapper">
<div className="menuitem">
<span className="dishname">{dish.title}</span>
<span className="dishprice">{dish.pricePerServing*modelInstance.getNumberOfGuests()}</span>
</div>
</div>
);
return (
<div id="sidebar-dishes">
{menu}
</div>
);
}
}
export default Sidebar;
IngredientsList.js:
class IngredientsList extends Component{
constructor(props){
super(props);
this.state = {
ingredients: props.ingredients,
pricePerServing: props.pricePerServing,
id: props.id,
noOfGuests: modelInstance.getNumberOfGuests(),
}
modelInstance.addObserver(this);
}
update(){
if(this._ismounted==true){
this.setState({
noOfGuests: modelInstance.getNumberOfGuests(),
});
}
}
componentDidMount(){
this._ismounted = true;
}
componentWillUnmount(){
this._ismounted = false;
}
render () {
return (
<button onClick={() => this.props.onButtonClick()} type="button" className="btn btn-default">Add to menu</button>
);
}
}
export default IngredientsList;
EDIT:
Changed DinneModel.addToMenu to:
this.addToMenu = function(id, type){
var newDish = this.getDish(id)
.then(()=>{
newDish.dishType = type;
selectedDishes.push(newDish);
notifyObservers();
});
}
I still get a promise logged in the console from the console.log in Sidebar.js, and NaN in the Sidebar render.
getDish is not in your code posted, but I assume that it returns a promise. And this.getDish(id).then() also returns a promise. That’s why selectedDishes array has promises in it.
this.addToMenu = function(id, type){
var newDish = this.getDish(id).then()
newDish.dishType = type;
selectedDishes.push(newDish);
notifyObservers();
}
To get actual newDish data, you need to use a callback function for the then.
this.addToMenu = function(id, type){
this.getDish(id).then(function (newDish) {
newDish.dishType = type;
selectedDishes.push(newDish);
notifyObservers();
});
}
The startUpload method inside <Items /> will call the callback function to update the state of the parent component each time it receives a response, This causes <Items /> to be rendered unnecessarily multiple times.
My expected effect is that after the state is updated, only the <Results /> component needs to be re-rendered
class Parent extends React.Component {
constructor(props) {
super(props);
this.getResponseData = this.getResponseData.bind(this);
this.state = {
responseData: [],
}
}
getResponseData(data) {
this.setState({
responseData: this.state.responseData.concat(data),
})
}
render() {
return (
<div>
<Items files={this.props.files} updateData={this.getResponseData}/>
<Results data={this.state.responseData}/>
</div>
)
}
}
class Items extends React.Component {
componentDidMount() {
this.startUpload(this.props.files)
}
startUpload(files) {
const URL = 'http://localhost:3000/upload';
for (let i = 0, len = files.length; i < len; i++) {
const data = new FormData();
data.append('img', files[i]);
fetch(URL, {
method: 'post',
body: data,
})
.then(checkStatus)
.then(parseJSON)
.then(data => {
this.props.updateData(data);
})
}
}
render() {
const filesData = this.getFilesData(this.props.files);
let imageItems = filesData.map((current) => {
return (
<div>
<img src={current.objectURL} alt="preview"/>
</div>
)
});
return <div>{imageItems}</div>;
}
}
function Results(props) {
const responseData = props.data;
let result = [];
if (responseData.length) {
result = responseData.map(current => {
return <p>{current}</p>
});
return <div>{result}</div>
}
}
https://facebook.github.io/react/docs/react-component.html#shouldcomponentupdate You can use shouldComponentUpdate to inform your component whether or not should re-render or not based on a change in state/props. Using this knowledge, you can implement the logic you need in order to render the Items/Results component only when needed.
Hope that helps!