React js control componentDidUpdate() method - javascript

I want to change a API parameter by click function and render new data. When I trigger componentDidUpdate by onclick event listener,the api data changed first and worked fine for first click. But When click second time the api call ran completely. The parameter currentPage is assigned to this.state.count and this this.state.count valued in incremented on click.
My code below:
import React from 'react';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
products: [],
count: 1,
};
}
componentDidMount() {
this.ProductList();
}
componentDidUpdate() {
let change = document.getElementById("change");
change.addEventListener("click",(e)=>{
this.changeParams();
this.ProductList();
})
}
changeParams = (e) =>{
this.setState({count: this.state.count + 1})
}
ProductList() {
var myHeaders = new Headers();
myHeaders.append("Cookie", "PHPSESSID=822cu5ctftcpo8f98ehklem4k9");
var requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
};
fetch("http://192.168.31.236/magento/rest/V1/products?searchCriteria[filterGroups][0][filters][0][field]=category_id& searchCriteria[filterGroups][0][filters][0][value]=2& searchCriteria[filterGroups][0][filters][0][conditionType]=eq&searchCriteria[sortOrders][0][field]=price& searchCriteria[sortOrders][0][direction]=ASC& searchCriteria[pageSize]=20& searchCriteria[currentPage]="+this.state.count, requestOptions)
.then(response => response.text())
.then(result => this.setState({products:result}),)
.catch(error => console.log('error', error));
}
render() {
const productsList = () =>{
let pro = [];
if(typeof this.state.products === 'string') {
pro = JSON.parse(this.state.products)
console.log(pro)
}else{
pro = []
}
if(pro.items && typeof pro.items !== "undefined"){
return pro.items.map((item, i) => (
<div>
<h1>{ item.name }</h1>
</div>
));
}
}
return(
<div>
{productsList()}
<button id="change">Change</button>
</div>
);
}
}
export default App;

Rather than manually attaching event listeners, do it through React. In pretty much most cases you shouldn't be doing DOM operations directly.
class App extends React.Component {
// ...
/* You don't need this
componentDidUpdate() {
}
*/
handleChangeClick = () => {
this.changeParams();
this.ProductList();
}
// ...
render() {
// ...
return(
<div>
{productsList()}
<button id="change" onClick={this.handleChangeClick}>Change</button>
</div>
);
}
}
The reason why your approach doesn't work is because React may be producing and destroying DOM elements in ways you don't expect, so making sure you manually attach and detach event listeners to the right elements is difficult to get right.

Related

I'm unable to change the array of an existing state

I'm trying to change one value inside a nested state.
I have a state called toDoItems that is filled with data with componentDidMount
The issue is that changing the values work and I can check that with a console.log but when I go to setState and then console.log the values again it doesn't seem like anything has changed?
This is all of the code right now
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
toDoItems: null,
currentView: "AllGroup"
};
}
componentDidMount = () => {
fetch("/data.json")
.then(items => items.json())
.then(data => {
this.setState({
toDoItems: [...data],
});
})
};
changeToDoItemValue = (givenID, givenKey, givenValue) => {
console.log(this.state.toDoItems);
let newToDoItems = [...this.state.toDoItems];
let newToDoItem = { ...newToDoItems[givenID - 1] };
newToDoItem.completedAt = givenValue;
newToDoItems[givenID - 1] = newToDoItem;
console.log(newToDoItems);
this.setState({
toDoItems: {newToDoItems},
})
console.log(this.state.toDoItems);
};
render() {
if (this.state.toDoItems) {
// console.log(this.state.toDoItems[5 - 1]);
return (
<div>
{
this.state.currentView === "AllGroup" ?
<AllGroupView changeToDoItemValue={this.changeToDoItemValue}/> :
<SpecificGroupView />
}
</div>
)
}
return (null)
};
}
class AllGroupView extends Component {
render() {
return (
<div>
<h1 onClick={() => this.props.changeToDoItemValue(1 , "123", "NOW")}>Things To Do</h1>
<ul className="custom-bullet arrow">
</ul>
</div>
)
}
}
So with my console.log I can see this happening
console.log(this.state.toDoItems);
and then with console.log(newToDoItems)
and then again with console.log(this.state.toDoitems) after setState
State update in React is asynchronous, so you should not expect updated values in the next statement itself. Instead you can try something like(logging updated state in setState callback):
this.setState({
toDoItems: {newToDoItems},// also i doubt this statement as well, shouldn't it be like: toDoItems: newToDoItems ?
},()=>{
//callback from state update
console.log(this.state.toDoItems);
})

React append component programmatically

I want to create a react component instance and render it in a static place programmatically.
My use-case is that I open a sequence of dialogs in an unknown length and when I get a response from a dialog I open the next.
I want to do something like:
const DialogExample = () => ({ question, onAnswer }) =>
(<div>
{question}
<button onClick={onAnswer}>answer</button>
</div>);
class SomeComponent extends React.Component {
async start() {
const questions = await getSomeDynamicQuestions();
this.ask(questions);
}
ask(questions) {
if (questions.length === 0) {
// DONE.. (do something here)
return;
}
const current = questions.pop();
React.magicMethod(
// The component I want to append:
<DialogExample
question={current}
onAnswer={() => this.ask(questions)}
/>,
// Where I want to append it:
document.getElementsByTagName('body')[0]);
}
render() {
return (
<div>
<button onClick={this.start}>start</button>
</div>);
}
}
I know that's not very "react-like", and I guess the "right" way of doing it will be storing those questions in state and iterate over them in "someComponent" (or other) render function, but still, I think that this pattern can make sense in my specific need.
Sounds like a case for Portals. I'd recommend doing something like this:
class SomeComponent extends React.Component {
constructor(props) {
super(props);
this.body = document.getElementsByTagName('body')[0];
this.state = {
questions: [],
}
}
async start() {
const questions = await getSomeDynamicQuestions();
this.setState({ questions });
}
nextQuestion() {
this.setState(oldState => {
const [first, ...rest] = oldState.questions;
return { questions: rest };
})
}
render() {
const { questions } = this.state;
return (
<div>
<button onClick={this.start}>start</button>
{questions.length > 0 && ReactDOM.createPortal(
<DialogExample
question={questions[0]}
onAnswer={() => this.nextQuestion()}
/>,
this.body,
)}
</div>
);
}
}

Promises in react.js, model attribute is promise instead of array

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();
});
}

how to distinguish which component called a callback function?

I'm new to react, sorry if that is newbe question. I have a component Dropdown which returns a value via a callback function. I would like to render that twice to choose two different values and then simply render chosen values below. How can I allow your two different components to send different data to the component. Below is my code.
index.js
import { Dropdown } from './components/dropdown'
class App extends Component {
constructor(props) {
super(props);
this.calculateRate = this.calculateRate.bind(this);
this.callApi = this.callApi.bind(this);
this.state = {
response: "",
currA: 0,
currB: 1
}
}
componentDidMount() {
this.callApi()
.then(res => this.setState({ response: res.express }))
.catch(err => {console.log(err)});
}
callApi = async () => {
const response = await fetch('/main');
const body = await response.json();
if (response.status !== 200) throw Error(body.message);
return body;
}
calculateRate = (key, val) => {
// if the calling agent sent currA data, update currA,
// else if the calling agent sent currB data, update currB
if (key === 'A') this.setState({currA: val})
if (key === 'B') this.setState({currB: val})
console.log('updated curr' + key + ' to ' + val);
}
render() {
return (
<div className='App'>
<div>
<Dropdown callbackFromParent={this.calculateRate}
stateKey={'A'} val={this.state.currA} />
<Dropdown callbackFromParent={this.calculateRate}
stateKey={'B'} val={this.state.currB} />
</div>
</div>
);
}
}
export default App;
dropdown.js
export class Dropdown extends React.Component {
constructor(props){
super(props);
this.state = {
list: [],
selected: ""
};
}
componentDidMount(){
fetch('https://api.fixer.io/latest')
.then(response => response.json())
.then(myJson => {
this.setState({ list: Object.keys(myJson.rates) });
});
}
render(){
var selectCurr = (curr) =>
<select
onChange={event => props.callbackFromParent(props.stateKey, event.target.value)}
>
{(this.state.list).map(x => <option>{x}</option>)}
</select>;
return (
<div>
{selectCurr()}
</div>
);
}
}
I'm not exactly sure what you're trying to achieve, but hopefully the following shows how you can allow your two different components to send different data to the <App> component.
The important changes are: we need to bind methods to the <App> component in the constructor() function, then we can use the .bind() method in the Dropdown component to specify the data to pass into the callback function:
import React, { Component } from 'react';
import './App.css';
class App extends Component {
constructor(props) {
super(props);
this.calculateRate = this.calculateRate.bind(this);
this.callApi = this.callApi.bind(this);
this.state = {
response: "",
currA: 0,
currB: 1
}
}
componentDidMount() {
/*
this.callApi()
.then(res => this.setState({ response: res.express }))
.catch(err => {console.log(err)});
*/
}
callApi = async () => {
const response = await fetch('/main');
const body = await response.json();
if (response.status !== 200) throw Error(body.message);
return body;
}
calculateRate = (key, val) => {
// if the calling agent sent currA data, update currA,
// else if the calling agent sent currB data, update currB
if (key === 'A') this.setState({currA: val})
if (key === 'B') this.setState({currB: val})
console.log('updated curr' + key + ' to ' + val);
}
render() {
return (
<div className='App'>
<div>
<Dropdown callbackFromParent={this.calculateRate}
stateKey={'A'} val={this.state.currA} />
<Dropdown callbackFromParent={this.calculateRate}
stateKey={'B'} val={this.state.currB} />
</div>
</div>
);
}
}
const Dropdown = props => (
<select onChange={event => props.callbackFromParent(props.stateKey, event.target.value)}>
<option value='cats'>Cats</option>
<option value='dogs'>Dogs</option>
</select>
)
export default App;

How to avoid repeated rendering of react component?

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!

Categories