Prop getting lost in unusual situation - javascript

I am trying to pass props from my parent component to my child component. When I receive the props in componentWillReceiveProps(), one of the children of my coin prop gets lost. This can be seen with the console.log lines in the child component.
For some reason, coin.profit prints "undefined" while printing just the coin object shows that coin.profit indeed is in the coin object. I have looked over my code for hours now, and asked friends to look at it and to no avail. Any help would be much appreciated.
Child Component (https://github.com/kdelalic/cryptofolio/blob/master/src/js/progress.js):
class Progress extends Component {
constructor(props) {
super(props);
this.state = {
initial: 0,
profit: 0,
holdings: 0,
change: "0%"
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.coins !== this.props.coins) {
Object.keys(nextProps.coins).map((key) => {
const coin = nextProps.coins[key]
console.log(coin)
console.log(coin.price)
console.log(coin.profit)
this.setState({
initial: this.state.initial + coin.price * coin.amount,
profit: this.state.profit,
holdings: this.state.profit + this.state.holdings,
change: this.state.initial / this.state.profit * 100 + "%",
})
})
}
}
Parent Component (https://github.com/kdelalic/cryptofolio/blob/master/src/js/crypto.js):
class Crypto extends Component {
constructor(props) {
super(props);
this.state = {
open: false,
};
}
getCurrentPrice = (key) => {
const { coins } = this.state;
var url = "https://min-api.cryptocompare.com/data/price?fsym=" + coins[key].value.substring(coins[key].value.indexOf("(")+1,coins[key].value.indexOf(")")).toUpperCase() + "&tsyms=" + coins[key].currency.toUpperCase();
axios.get(url)
.then(response => {
const price = response.data[coins[key].currency.toUpperCase()];
const profit = parseFloat((price - coins[key].price) * coins[key].amount).toFixed(2)
var newState = this.state;
newState.coins[key]["currentPrice"] = price;
newState.coins[key]["profit"] = profit;
this.setState(newState);
})
.catch(err => {
console.log(err)
});
};
checkPos = (num) => {
if (num > 0) {
return " positive"
} else if (num < 0) {
return " negative"
} else {
return ""
}
};
handleOpen = () => {
this.setState({ ...this.state, open: true });
};
handleClose = () => {
this.setState({ ...this.state, open: false });
};
coinData = (dataFromChild, key) => {
const newCoins = {
...this.state.coins
};
newCoins[key] = dataFromChild
this.setState({
...this.state,
coins: newCoins
}, () => {
this.getCurrentPrice(key);
this.setState({
...this.state,
})
this.handleClose();
})
};
render() {
const { coins } = this.state;
return (
<div className="crypto">
<Progress coins={this.state.coins}/>

In React, you should never mutate the existing state. In
var newState = this.state;
newState.coins[key]["currentPrice"] = price;
newState.coins[key]["profit"] = profit;
this.setState(newState);
you are never creating any new objects. You should be doing
this.setState({
...this.state,
coins: {
...this.state.coins,
[key]: {
...this.state.coins[key],
currentPrice: price,
profit,
},
},
});
to create new state objects for each item you are mutating.
Because you are modifying the existing object, means the object passed to componentWillReceiveProps will potentially be updated by your other code.

Related

Uncaught Error: This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate

I am trying to setState inside componentWillUpdate But it is giveing me error saying this.I don't know why.
Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
This is how I am trying to do .
class DispensingIncidents extends React.Component {
constructor(props) {
super(props);
this.state = {
timeSpan: "",
year: 2020,
tabValue: "",
spanData: [],
dataType: "",
durationList: "",
durationLabel: "",
dataTo: "",
dataFrom: "",
incidencesSection: false,
overViewSelect: "",
selectTab: 0,
};
}
componentDidMount() {
this.props.getFilterData(this.state.year);
this.props.getDuration(this.state.year);
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (
prevProps.dispensingData.duration !== this.props.dispensingData.duration
) {
this.props.getOverviewData(
Object.keys(this.props.dispensingData.period)[3],
this.props.dispensingData.duration.monthly[0].period.to,
this.props.dispensingData.duration.monthly[0].period.from,
Object.keys(this.props.dispensingData.type)[0],
this.props.dispensingData._reference
);
this.setState({
timeSpan: Object.keys(this.props.dispensingData.period)[3],
dataType: Object.keys(this.props.dispensingData.type)[0],
});
}
}
componentWillReceiveProps() { < ---- I was trying this as well but gives same error.
// console.log("componentWillReceiveProps", nextState);
// if (this.state.timeSpan === "monthly") {
// const monthlyData = this.props.dispensingData.duration["monthly"];
// console.log({ monthlyData });
// this.setState({ spanData: monthlyData });
// }
// if (this.state.timeSpan === "yearly") {
// const yearlyData = this.props.dispensingData.duration["yearly"];
// this.setState({ spanData: yearlyData });
// } else if (this.state.timeSpan === "weekly") {
// const weeklyData = this.props.dispensingData.duration["weekly"];
// this.setState({ spanData: weeklyData });
// } else if (this.state.timeSpan === "quarterly") {
// const quarterlyData = this.props.dispensingData.duration["quarterly"];
// this.setState({ spanData: quarterlyData });
// } else if (this.state.timeSpan === "monthly") {
// const monthlyData = this.props.dispensingData.duration["monthly"];
// console.log({ monthlyData });
// this.setState({ spanData: monthlyData });
// } else if (this.state.timeSpan === "6 months") {
// const halfYearlyData = this.props.dispensingData.duration["half-yearly"];
// this.setState({ spanData: halfYearlyData });
// }
}
handleTabChange = (event, newValue) => {
this.setState({
setTabValue: newValue,
});
};
/**
* Year change
* #param {*} event
*/
handleYearChange = (event) => {
const v = event.target.value;
this.setState({
year: v,
});
this.props.getDuration(v);
};
handleSpanChange = (event) => {
const value = event.target.value;
const reValue = value === "6 months" ? "half-yearly" : value;
this.setState({
timeSpan: value,
});
this.props.getOverviewData(
value,
this.props.duration[reValue][0].period.to,
this.props.duration[reValue][0].period.from,
this.props.dataType,
this.props._reference
);
this.setState({
dataTo: this.props.duration[reValue][0].period.to,
dataFrom: this.props.duration[reValue][0].period.from,
incidencesSection: false,
overViewSelect: "",
});
};
handleSpanTabChange = (data, i) => {
this.setState({
selectTab: i,
dataTo: data.period.to,
dataFrom: data.period.from,
durationLabel: data.label,
overViewSelect: "",
incidencesSelection: false,
});
};
handleDataTypeChange = (event) => {
this.setState({
dataType: event.target.value,
});
const { time, dataTo, dataFrom } = this.state;
this.props.getOverviewData(
time,
dataTo,
dataFrom,
event.target.value,
this.props._reference
);
this.setState({
incidencesSection: false,
overViewSelect: "",
});
};
handleOverViewClick = (data) => {
this.props.clearAnalysis();
this.setState({
overViewSelect: data,
});
const value =
this.state.time === "6 months" ? "half_yearly" : this.state.time;
const { dataFrom, dataTo, dataType } = this.state;
this.props.getAnalysis(
data,
value,
dataFrom,
dataTo,
dataType,
1,
this.props._reference
);
this.setState({
incidencesSelection: true,
});
};
render() {
const {
tabValue,
spanData,
timeSpan,
year,
dataType,
overViewSelect,
incidencesSection,
} = this.state;
const { classes, dispensingData } = this.props;
const {
duration,
period,
type,
incidenceAnalysisDataArray,
_reference,
overviewDataLoading,
loading,
dispensingOverviewData,
analysisDataLoading,
incidenceAnalysisData,
} = dispensingData;
const { count } = dispensingOverviewData;
console.log("handleTimeSPan", this.state.spanData);
return (
<div className={classes.dispenseRoot}>
</div>
);
}
}
I am using redux saga in my project.
Well, You change the state and component rerender itself.
Rerender calls componentWillUpdate, it changes state and component rerender itself.
Rerender calls componentWillUpdate, it changes state and component rerender itself.
Rerender calls componentWillUpdate, it changes state and component rerender itself.
Rerender calls componentWillUpdate, it changes state and component rerender itself.

React.js : Updating State of Nested Object

Front End - Front End
Upon clicking the star, I want to update the state of nested object, with the new rating value of star.
I tried many things but it didnt work as states are immutable.
Nested State
Can some upon please suggest how can I update the value in nested object
onStarClicked = (kTypName, subItemId1, newRating) => {
//console.log(subItemId.split("_"));
let evaluation = subItemId1.split("_")[0];
let subItemId = subItemId1.split("_")[1];
console.log(subItemId);
const r = { ...this.state.ratings };
let kT = r.knowledgeTypes;
let sub = '', kTN = '', kIN = '';
kT.map(knowledgeType => {
//console.log(knowledgeType.knowledgeTypeId);
knowledgeType.knowledgeItems.map(knowledgeItem => {
//console.log(knowledgeItem.knowledgeItemId);
knowledgeItem.subItems.map(knowledgeSubItem => {
//console.log(knowledgeSubItem.subItemId);
if (subItemId === knowledgeSubItem.subItemId) {
kTN = knowledgeType.knowledgeTypeName;
kIN = knowledgeItem.knowledgeItemName;
sub = knowledgeSubItem;
if (evaluation === "self") {
sub.evaluation.self.rating = newRating;
}
else if (evaluation === "evaluator") {
sub.evaluation.evaluator.rating = newRating;
}
//alert(evaluation + subItemId + ' ' + newRating);
//return;
}
})
})
});
this.setState({
...this.state,
ratings: {
...this.state.ratings,
knowledgeTypes: [
...this.state.ratings.knowledgeTypes,
this.state.ratings.knowledgeTypes.filter(kt => kt.knowledgeTypeName !== kTN),
{
...this.state.ratings.knowledgeTypes.knowledgeItems.
filter(ki => ki.knowledgeItemName !== kIN),
knowledgeItems: {
...this.state.ratings.knowledgeTypes.knowledgeItems.subItems.
filter(si => si.subItemId !== subItemId),
sub
}
}]
}
});
}
You basically have to create a new empty array of knowledgeTypes and use the current state to find which item of the state you need to change using Object.keys/map/filter functions.
You'd use the current state in a variable and modify that variable only. You'd likely not mess with the actual state object in any way.
After you have done that, simply append it to the empty array. Then you can setState() the new array to the actual state property.
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
financialYear: "2019-20",
quarter: "Q1",
isCurrentQuarter: true,
knowledgeTypes: [
{
knowledgeTypeName: "Technology",
knowledgeItems: [
{
knowledgeItemName: "Java",
subItems: [
{
subItemId: "2",
subItemName: "Collections",
evaluation: {
self: {
ntnet: "Joe",
rating: 1,
isEditable: true
}
}
}
]
}
]
}
]
};
}
handleClick = e => {
const { knowledgeTypes } = this.state;
// transformation
const itemToChange = knowledgeTypes.map(item => {
if (item.knowledgeTypeName === "Technology") {
return item;
}
});
const currItems = itemToChange[0].knowledgeItems[0].subItems;
const subItem = currItems.map(item => {
if (item.subItemId === "2") {
return item;
}
});
const person = subItem[0].evaluation;
person.self.rating = 55; //change
const newKnowledgeTypes = [];
knowledgeTypes.map(item => {
if (item.knowledgeTypeName === "Technology") {
newKnowledgeTypes.push(itemToChange);
}
newKnowledgeTypes.push(item);
});
this.setState({
knowledgeTypes: newKnowledgeTypes
});
console.log(this.state);
};
render() {
return (
<div>
MyComponent
<button onClick={this.handleClick}>Hello</button>
</div>
);
}
}
The sandbox can be found on https://codesandbox.io/s/musing-dew-8r2vk.
Note: It is advisable you do not use nested state objects because state objects are something more lightweight so that they do not have performance considerations.
import React, { Component } from 'react';
import Auxilary from '../../../hoc/Auxilary/auxilary';
import KnowledgeItems from '../KnowledgeItems/KnowledgeItems';
import Tabs from 'react-bootstrap/Tabs';
import Tab from 'react-bootstrap/Tab';
import knowledge from '../../../assests/staticdata.json';
import './QuarterLog.css';
class QuarterLog extends Component {
constructor() {
super();
this.state = {
"financialYear": "",
"quarter": "",
"isCurrentQuarter": "",
"knowledgeTypes": []
}
}
onStarClicked = (kTypName, kItemName, subItemIdName, newRating) => {
let evaluation = subItemIdName.split("_")[0];
let subItemId = subItemIdName.split("_")[1];
const { knowledgeTypes } = this.state;
// transformation
let knowledgeTypeToChange = knowledgeTypes.map(kType => {
if (kType.knowledgeTypeName === kTypName) {
return kType;
}
});
knowledgeTypeToChange = knowledgeTypeToChange.filter(function (element) {
return element !== undefined;
});
console.log(knowledgeTypeToChange[0]);
let knowledgeItemToChange = knowledgeTypeToChange[0].knowledgeItems.map(item => {
if (item.knowledgeItemName === kItemName) {
return item;
}
});
knowledgeItemToChange = knowledgeItemToChange.filter(function (element) {
return element !== undefined;
});
let knowledgeSubItem = knowledgeItemToChange[0].subItems.map(subItem => {
if (subItem.subItemId === subItemId) {
return subItem;
}
});
knowledgeSubItem = knowledgeSubItem.filter(function (element) {
return element !== undefined;
});
console.log(knowledgeSubItem);
let personEvaluations = knowledgeSubItem[0].evaluation;
if (evaluation === "self") {
personEvaluations.self.rating = newRating.toString(); //change
}
else if (evaluation === "evaluator") {
personEvaluations.evaluator.rating = newRating.toString(); //change
}
const newKnowledgeTypes = [];
knowledgeTypes.map(item => {
if (item.knowledgeTypeName === kTypName) {
newKnowledgeTypes.push(knowledgeTypeToChange[0]);
}
else
newKnowledgeTypes.push(item);
});
this.setState({
knowledgeTypes: newKnowledgeTypes
});
console.log(this.state);
}
componentDidMount() {
// TODO: remove staticdata.js and call REST API and set the response in state
this.setState({
...this.state,
"financialYear": knowledge.financialYear,
"quarter": knowledge.quarter,
"isCurrentQuarter": knowledge.isCurrentQuarter,
"knowledgeTypes": knowledge.knowledgeTypes
})
}
onSubmitRatings = () => {
console.log(this.state);
}
render() {
let data = knowledge; //remove this code, once REST API is implemented
const posts = this.state.knowledgeTypes.map(knowledgeType => {
return (
<Tab key={knowledgeType.knowledgeTypeName} eventKey={knowledgeType.knowledgeTypeName}
title={knowledgeType.knowledgeTypeName}>
<KnowledgeItems
kTypeName={knowledgeType.knowledgeTypeName}
kItems={knowledgeType.knowledgeItems}
ratings={this.state.ratings}
onstarclicked={this.onStarClicked}
/>
</Tab>)
});
return (
<Auxilary>
<div className="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<div><h1>Financial Year : {data.financialYear}</h1></div>
<div><h2>Quarter : {data.quarter}</h2></div>
</div>
<div>
<Tabs defaultActiveKey="Domain" id="uncontrolled-tab-example">
{posts}
</Tabs>
</div>
<button onClick={this.onSubmitRatings}> Submit </button>
</Auxilary>
);
}
}
export default QuarterLog;

Component did update returning always the same props and state

I found a lot of solutions about this problem but none of them work.
I have a view which renders dynamically components depending on the backend response
/**
* Module dependencies
*/
const React = require('react');
const Head = require('react-declarative-head');
const MY_COMPONENTS = {
text: require('../components/fields/Description'),
initiatives: require('../components/fields/Dropdown'),
vuln: require('../components/fields/Dropdown'),
severities: require('../components/fields/Dropdown'),
};
const request = restclient({
timeout: 5000,
baseURL: '/api',
});
const { DropdownItem } = Dropdown;
class CreateView extends React.Component {
constructor(props) {
super(props);
this.state = {
modal: false,
states: props.states,
error: props.error,
spinner: true,
state: props.state,
prevState: '',
components: [],
};
this.handleChange = this.handleChange.bind(this);
this.getRequiredFields = this.getRequiredFields.bind(this);
this.onChangeHandler = this.onChangeHandler.bind(this);
this.changeState = this.changeState.bind(this);
this.loadComponents = this.loadComponents.bind(this);
}
componentDidMount() {
this.loadComponents();
}
onChangeHandler(event, value) {
this.setState((prevState) => {
prevState.prevState = prevState.state;
prevState.state = value;
prevState.spinner = true;
return prevState;
}, () => {
this.getRequiredFields();
});
}
getRequiredFields() {
request.get('/transitions/fields', {
params: {
to: this.state.state,
from: this.state.prevState,
},
})
.then((response) => {
const pComponents = this.state.components.map(c => Object.assign({}, c));
pComponents.forEach((c) => {
c.field.required = 0;
c.field.show = false;
});
response.data.forEach((r) => {
const ob = pComponents.find(c => c.field.name === r.name);
if (ob) {
ob.field.required = r.required;
ob.field.show = true;
}
});
this.setState({
components: pComponents,
fields: response.data,
spinner: false,
});
})
.catch(err => err);
}
loadComponents() {
this.setState((prevState) => {
prevState.components = Object.keys(MY_COMPONENTS).map((k) => {
const field = {
name: k,
required: 0,
show: true,
};
return {
field, component: MY_COMPONENTS[k],
};
});
return prevState;
});
}
handleChange(field, value) {
this.setState((prevState) => {
prevState[field] = value;
return prevState;
});
}
changeState(field, value) {
this.setState((prevState) => {
prevState[`${field}`] = value;
return prevState;
});
}
render() {
const Components = this.state.components;
return (
<Page name="CI" state={this.props} Components={Components}>
<Script src="vendor.js" />
<Card className="">
<div className="">
<div className="">
<Spinner
show={this.state.spinner}
/>
{Components.map((component, i) => {
const Comp = component.component;
return (<Comp
key={i}
value={this.state[component.field.name]}
field={component.field}
handleChange={this.handleChange}
modal={this.state.modal}
changeState={this.changeState}
/>);
})
}
</div>
</div>
</div>
</Card>
</Page>
);
}
}
module.exports = CreateView;
and the dropdown component
const React = require('react');
const request = restclient({
timeout: 5000,
baseURL: '/api',
});
const { DropdownItem } = Dropdown;
class DrpDwn extends React.Component {
constructor(props) {
super(props);
this.state = {
field: props.field,
values: [],
};
}
componentDidUpdate(prevProps, prevState, snapshot) {
console.log('state', this.state.field);
console.log('prevState', prevState.field);
console.log('prevProps', prevProps.field);
console.log('props', this.props.field);
}
render() {
const { show } = this.props.field;
return (show && (
<div className="">
<Dropdown
className=""
onChange={(e, v) => this.props.handleChange(this.props.field.name, v)}
label={this.state.field.name.replace(/^./,
str => str.toUpperCase())}
name={this.state.field.name}
type="form"
value={this.props.value}
width={100}
position
>
{this.state.values.map(value => (<DropdownItem
key={value.id}
value={value.name}
primary={value.name.replace(/^./, str => str.toUpperCase())}
/>))
}
</Dropdown>
</div>
));
}
module.exports = DrpDwn;
The code actually works, it hide or show the components correctly but the thing is that i can't do anything inside componentdidupdate because the prevProps prevState and props are always the same.
I think the problem is that I'm mutating always the same object, but I could not find the way to do it.
What I have to do there is to fill the dropdown item.
Ps: The "real" code works, i adapt it in order to post it here.
React state is supposed to be immutable. Since you're mutating state, you break the ability to tell whether the state has changed. In particular, i think this is the main spot causing your problem:
this.setState((prevState) => {
prevState.components = Object.keys(MY_COMPONENTS).map((k) => {
const field = {
name: k,
required: 0,
show: true,
}; return {
field, component: MY_COMPONENTS[k],
};
});
return prevState;
});
You mutate the previous states to changes its components property. Instead, create a new state:
this.setState(prevState => {
const components = Object.keys(MY_COMPONENTS).map((k) => {
const field = {
name: k,
required: 0,
show: true,
};
return {
field, component: MY_COMPONENTS[k],
};
});
return { components }
}
You have an additional place where you're mutating state. I don't know if it's causing your particular problem, but it's worth mentioning anyway:
const pComponents = [].concat(this.state.components);
// const pComponents = [...this.state.components];
pComponents.forEach((c) => {
c.field.required = 0;
c.field.show = false;
});
response.data.forEach((r) => {
const ob = pComponents.find(c => c.field.name === r.name);
if (ob) {
ob.field.required = r.required;
ob.field.show = true;
}
});
You do at make a copy of state.components, but this will only be a shallow copy. The array is a new array, but the objects inside the array are the old objects. So when you set ob.field.required, you are mutating the old state as well as the new.
If you want to change properties in the objects, you need to copy those objects at every level you're making a change. The spread syntax is usually the most succinct way to do this:
let pComponents = this.state.components.map(c => {
return {
...c,
field: {
...c.field,
required: 0,
show: false
}
}
});
response.data.forEach(r => {
const ob = pComponents.find(c => c.field.name === r.name);
if (ob) {
// Here it's ok to mutate, but only because i already did the copying in the code above
ob.field.required = r.required;
ob.field.show = true;
}
})

Component only updates after two clicks React

I am building a React app that - among other things - generates a random number when a button is clicked and then filters an array of JSON objects to only the one at the index of that random number (i.e. JSON[random]). Normally the app is supposed to re-render after the array of JSON objects is filtered, but for some reason, on the first time the button is clicked and a random is picked, it requires two clicks to update. From then on it updates as expected, with a new random rendering each time the button is clicked.
I'm not sure if the problem is coming from App.js or somewhere lower down. On the first click, it generates a new random and supposedly saves this to state, but fails to re-render right away. On subsequent clicks, things seem to update based on the previously-generated random, while a new random is put in the queue. I would prefer the this all happens in one go: click, generate random, save to state, update to reflect the new random à la JSON[random].
This might have something to do with the way I have implemented lifecycle methods, as I'm admittedly not sure of all the nuances of each and have just tried to use whichever ones seemed to do what I wanted. If you have any suggestions there, please let me know...
Thanks!
Here are the relevant files:
App.js - where the random is generated and stored when a new click is registered in Header.state.randomClicks
class App extends Component {
constructor(props){
super(props)
this.state = {headerLink: "", searchValue: "", random: 0, randomClicks: 0}
this.generateRandom = this.generateRandom.bind(this);
}
getLinkFromHeader = (link) => {
if (this.state.headerLink !== link) {
this.setState({
headerLink: link,
})
}
}
getSearchValueFromHeader = (string) => {
this.setState({
searchValue: string,
});
}
getRandomMax = (max) => {
this.setState({
randomMax: max,
})
}
getRandomClicks = (value) => {
this.setState({
randomClicks: value,
})
}
generateRandom(number) {
let random = Math.floor(Math.random() * number) + 1;
console.log("generateRandom = ", random)
return random
}
shouldComponentUpdate(nextProps, nextState) {
return this.state.randomClicks !== nextState.randomClicks;
}
componentWillUpdate() {}
componentDidUpdate(prevState) {
let randomClicks = this.state.randomClicks;
console.log("this.state.randomClicks: ", this.state.randomClicks)
// console.log("prevState: ", prevState)
// console.log("prevState.randomClicks = ", prevState.randomClicks)
// ^^ is this a bug ? ^^
let random = this.generateRandom(this.state.randomMax);
if (this.state.random !== random) {
this.setState({random: random})
}
}
render() {
return (
<div className="App background">
<div className="content">
<Header getLinkFromHeader={this.getLinkFromHeader} getSearchValueFromHeader={this.getSearchValueFromHeader} randomClick={this.randomClick} getRandomClicks={this.getRandomClicks}/>
<TilesContainer link={this.state.headerLink} searchValue={this.state.searchValue} getRandomMax={this.getRandomMax} random={this.state.random} randomClicks={this.state.randomClicks}/>
</div>
</div>
);
}
}
export default App
Header.js* - where the randomClick count is incremented each time RandomButton is clicked
class Header extends Component {
constructor(props){
super(props);
this.state = { selectorLink: "", searchValue: "", randomClicks: 0 }
this.randomClick = this.randomClick.bind(this);
}
getLinkFromSelector = (link) => {
this.setState({
selectorLink: link,
})
}
getSearchValue = (string) => {
this.setState({
searchValue: string,
})
}
shouldComponentUpdate(nextProps, nextState) {
console.log("this.state !== nextState: ", this.state !== nextState)
return this.state !== nextState;
}
componentDidUpdate(previousState){
if(this.state.selectorLink !== previousState.selectorLink) {
this.props.getLinkFromHeader(this.state.selectorLink);
}
this.props.getSearchValueFromHeader(this.state.searchValue);
this.props.getRandomClicks(this.state.randomClicks);
console.log("Header Did Update")
}
randomClick(){
this.props.randomClick;
this.setState({
randomClicks: this.state.randomClicks += 1,
});
}
render(){
return(
<div id="header" className="header">
<div className="title-div">
<div className="h1-wrapper title-wrapper">
<h1>Pokédex Viewer App</h1>
</div>
</div>
<PokedexSelector getLinkFromSelector={this.getLinkFromSelector}/>
<SearchBar getSearchValue={this.getSearchValue}/>
<button type="button" id="random-button" onClick={this.randomClick}>Random Pokémon</button>
<button type="button" id="show-all-button" onClick={this.showAllClick}>Show All</button>
</div>
)
}
}
export default Header
TilesContainer.js - where the random number from App is sent and the tiles list is filtered/re-rendered
class TilesContainer extends Component {
constructor(props){
super(props);
this.state = {
pokemon: [],
filteredPokemon: [],
randomMax: 0,
showDetails: false,
};
this.getPokemon = this.getPokemon.bind(this);
this.tiles = this.tiles.bind(this);
this.getPokemon(this.props.link);
}
getPokemon(pokedexLink) {
let link = "";
(pokedexLink === "")
? link = "https://pokeapi.co/api/v2/pokedex/national/"
: link = this.props.link;
fetch(link)
.then(response => response.json())
.then(myJson => {
let list = myJson['pokemon_entries'];
this.setState({
pokemon: list,
randomMax: list.length,
})
this.props.getRandomMax; // send randomMax to App
})
}
filterPokemon(string) {
if (string !== "") {
console.log("string: ", string)
string = string.toString().toLowerCase()
let filteredPokemon = this.state.pokemon.filter(pokemon => {
const name = pokemon.pokemon_species.name;
const nameStr = name.slice(0,string.length);
const number = pokemon.entry_number;
const numberStr = number.toString().slice(0, string.length);
return (this.state.random !== 0) ? number.toString() === string : nameStr === string || numberStr === string;
})
if (this.props.randomClicks !== 0) { // i.e. using a random
this.setState({
filteredPokemon: filteredPokemon,
})
} else {
this.setState({
filteredPokemon: filteredPokemon,
randomMax: filteredPokemon.length,
})
}
} else {
this.setState({
filteredPokemon: [],
randomMax: this.state.pokemon.length,
})
}
}
componentDidUpdate(prevProps, prevState) {
if (this.props.link !== prevProps.link) {
this.getPokemon(this.props.link)
}
if (this.props.searchValue !== prevProps.searchValue) {
this.filterPokemon(this.props.searchValue)
}
if (this.state.randomMax !== prevState.randomMax){
this.props.getRandomMax(this.state.randomMax);
}
if (this.props.random !== prevProps.random) {
console.log("TilesContainer random: ", this.props.random)
this.filterPokemon(this.props.random)
}
}
tiles() {
console.log("tiles() filteredPokemon: ", this.state.filteredPokemon)
console.log("tiles() searchValue: ", this.props.searchValue)
console.log("tiles() random: ", this.props.random)
if (this.state.pokemon.length > 0) {
if (this.state.filteredPokemon.length == 0 && this.props.searchValue === ""){
return (
this.state.pokemon.map(pokemon => (
<Tile key={pokemon.entry_number} number={pokemon.entry_number} name={pokemon.pokemon_species.name} url={pokemon.pokemon_species.url}/>
))
)
} else if (this.state.filteredPokemon.length > 0){
return (
this.state.filteredPokemon.map(pokemon => (
<Tile key={pokemon.entry_number} number={pokemon.entry_number} name={pokemon.pokemon_species.name} url={pokemon.pokemon_species.url}/>
))
)
}
}
}
render(){
return (
<div id="tiles-container"
className="tiles-container">
{this.tiles()}
</div>
)
}
}
export default TilesContainer
You should not use current state in setState and should not modify state directly. And you do no actually call this.props.randomClick and it is undefined. Change
randomClick(){
this.props.randomClick;
this.setState({
randomClicks: this.state.randomClicks += 1,
});
}
to
randomClick(){
if (typeof(this.props.randomClick) === 'function') this.props.randomClick();
this.setState(olState => ({
randomClicks: olState.randomClicks + 1,
}));
}
Also check your shouldComponentUpdate methods. They might be buggy or redundant. Looks like you prevent updating App when state.random changes. So every time you click the button you store the new random value but use the previous one. So for the initial render and for the first click you use random: 0.
And I guess that getRandomClicks should be setRandomClicks.

Bloated state in React

I have a React app with a bunch of components with a few similarities:
Most components load data from Firebase at construction
Most components have an input form that the user can interact with
Most components have a simple view
My issue is that the state becomes hard to manage fairly early on as I try to keep all state in the top level component. For instance, I have the component below that let's the user create a new product, add a few images and place a custom marker on one of the images.
My current setup for all components is that there is a currentEntry which represents the entry that the user is currently editing which I initialize with a blank state.
Is it best practice to keep all state in the top component like this or should I rethink my structure?
import React, { Component } from 'react';
import CreateEntryForm from "../../components/entries/createEntryForm";
import { withStyles } from 'material-ui/styles';
import ViewImageDialog from "../../components/entries/viewImageDialog";
import {FirebaseList} from "../../utils/firebase/firebaseList";
import {generateFilename, removeItem, snapshotToArray} from "../../utils/utils";
import {
Redirect
} from 'react-router-dom';
import AppBar from "../../components/appBar";
import Spinner from "../../components/shared/spinner";
import firebase from 'firebase';
const styles = theme => ({
root: {
margin: theme.spacing.unit*2,
}
});
const initialFormState = {
currentProduct: null,
selectedProducts: [],
selectedUploads: [],
selectedMarkedImage: null,
productQuantity: '',
locationDescription: '',
comments: '',
currentUpload: null,
username: 'username'
};
const initialFormErrorState = {
selectProductError: '',
};
class CreateEntry extends Component {
constructor() {
super();
this.state = {
products: [],
job: null,
currentEntry: {...initialFormState},
formErrors: initialFormErrorState,
uploadLoading: false,
markedImageLoaded: false,
attachmentDialogOpen: false,
openAttachment: null,
markerPosition: null,
availableAttachments: [],
entries: [],
redirect: false,
loading: true,
isEditing: false
};
this.firebase = new FirebaseList('entries');
this.handleSubmit = this.handleSubmit.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
this.setMarker = this.setMarker.bind(this);
this.handleAttachmentDialogOpen = this.handleAttachmentDialogOpen.bind(this);
this.saveMarkedImage = this.saveMarkedImage.bind(this);
this.handleMarkedImageLoaded = this.handleMarkedImageLoaded.bind(this);
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleProgress = this.handleProgress.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
this.handleUploadSuccess = this.handleUploadSuccess.bind(this);
}
componentDidMount() {
this.firebase.path = `entries/${this.props.match.params.id}`;
this.jobId = this.props.match.params.id;
this.entryId = this.props.match.params.entry || null;
this.firebase.db().ref(`jobs/${this.props.match.params.id}`).on('value', (snap) => {
const job = {
id: snap.key,
...snap.val()
};
this.setState({
job: job,
loading: false,
})
});
this.firebase.databaseSnapshot(`attachments/${this.jobId}`).then((snap) => {
const attachments = snapshotToArray(snap);
this.setState({availableAttachments: attachments})
});
this.firebase.databaseSnapshot(`entries/${this.jobId}`).then((snap) => {
const entries = snapshotToArray(snap);
const otherMarkedEntries = entries.filter(entry => entry.id !== this.entryId);
this.setState({otherMarkedEntries: otherMarkedEntries})
});
if (this.entryId) {
this.firebase.databaseSnapshot(`entries/${this.jobId}/${this.entryId}`).then((entry) => {
const updatedEntry = Object.assign({...initialFormState}, entry.val());
this.setState({
currentEntry: updatedEntry,
isEditing: !!this.entryId
})
});
}
}
validate() {
const errors = {...initialFormErrorState};
let isError = false;
if(this.state.currentEntry.selectedProducts.length === 0) {
errors.selectProductError = "You must select at least one product";
isError = true;
}
this.setState({formErrors: errors});
return isError
}
handleSubmit() {
const err = this.validate();
if(!err) {
if(this.state.job && this.state.currentEntry) {
if(!this.state.isEditing) {
const newEntry = {
...this.state.currentEntry,
'creationDate': Date.now()
};
let newEntryRef = this.firebase.db().ref(`entries/${this.jobId}`).push();
newEntryRef.set(newEntry);
if (this.state.currentEntry.selectedMarkedImage !== null) {
this.firebase.db().ref(`attachments/${this.jobId}/${newEntry.currentUpload.id}/markings/${newEntryRef.key}`)
.set(this.state.currentEntry.selectedMarkedImage)
}
this.setState({redirect: 'create'});
} else {
const updatedEntry = {
...this.state.currentEntry
};
const newLogEntry = {
'lastUpdated': Date.now(),
'updatedBy': 'username'
};
this.firebase.db().ref(`log/${this.jobId}/${this.entryId}`).push(newLogEntry);
this.firebase.update(this.entryId, updatedEntry)
.then(() => this.setState({redirect: 'edit'}));
}
}
}
};
handleInputChange = name => e => {
e.preventDefault();
const target = e.target;
const value = target.value;
if (name === 'currentUpload') {
this.handleAttachmentDialogOpen(this.state.job.selectedUploads);
}
this.setState({ currentEntry: { ...this.state.currentEntry, [name]: value } });
};
addSelectedChip = () => {
if (this.state.currentEntry.currentProduct) {
const updatedCurrentProduct = {
...this.state.currentEntry.currentProduct,
'productQuantity': this.state.currentEntry.productQuantity
};
const updatedSelectedProducts = [...this.state.currentEntry.selectedProducts, updatedCurrentProduct];
const updatedEntryStatus = {
...this.state.currentEntry,
selectedProducts: updatedSelectedProducts,
currentProduct: null,
productQuantity: ''
};
this.setState({currentEntry: updatedEntryStatus});
}
};
handleRequestDeleteChip = (data, group) => {
const itemToChange = new Map([['product', 'selectedProducts'], ['upload', 'selectedUploads']]);
const selected = itemToChange.get(group);
const updatedSelectedItems = removeItem(this.state.currentEntry[selected], data.id);
const updatedEntryStatus = {
...this.state.currentEntry,
[selected]: updatedSelectedItems
};
this.setState({currentEntry: updatedEntryStatus});
};
handleAttachmentDialogOpen = (attachment) => {
this.setState({
attachmentDialogOpen: true,
openAttachment: attachment
});
};
handleAttachmentDialogClose =() => {
this.setState({attachmentDialogOpen: false})
};
saveMarkedImage() {
const markedImage = {
'attachment': this.state.openAttachment[0],
'position': this.state.markerPosition
};
const updatedCurrentEntry = {
...this.state.currentEntry,
'selectedMarkedImage': markedImage
};
this.setState({
currentEntry: updatedCurrentEntry
});
this.handleAttachmentDialogClose()
}
setMarker(e) {
const dim = e.target.getBoundingClientRect();
const position = {
'pageX': e.pageX - dim.left -25,
'pageY': e.pageY - dim.top - 50
};
this.setState({markerPosition: position});
}
handleMarkedImageLoaded() {
this.setState({markedImageLoaded: true})
}
filterProducts(selected, available) {
if(this.state.job) {
const selectedProductNames = [];
selected.forEach(product => selectedProductNames.push(product.name));
return available.filter(product => !selectedProductNames.includes(product.name))
}
}
handleUploadStart = () => this.setState({uploadLoading: true, progress: 0});
handleProgress = (progress) => this.setState({progress});
handleUploadError = (error) => {
this.setState({uploadLoading: false});
console.error(error);
};
handleUploadSuccess = (filename) => {
firebase.storage().ref('images').child(filename).getDownloadURL().then(url => {
const getNameString = (f) => f.substring(0,f.lastIndexOf("_"))+f.substring(f.lastIndexOf("."));
const uploadItem = {"name": getNameString(filename), "url": url, "id": this.generateRandom()};
const updatedSelectedUploads = [...this.state.currentEntry.selectedUploads, uploadItem];
const updatedEntryStatus = {
...this.state.currentEntry,
selectedUploads: updatedSelectedUploads
};
this.setState({
uploadLoading: false,
currentEntry: updatedEntryStatus
});
});
};
generateRandom() {
return parseInt(Math.random());
}
render() {
const {classes} = this.props;
const filteredProducts = this.filterProducts(this.state.currentEntry.selectedProducts, this.state.job && this.state.job.selectedProducts);
const title = this.state.isEditing ? "Edit entry for" : "Add entry for";
const redirectRoute = this.state.redirect
? `/entries/${this.props.match.params.id}/${this.state.redirect}`
: `/entries/${this.props.match.params.id}`;
return (
<section>
<AppBar title={`${title} ${this.state.job && this.state.job.jobId}`} route={`/entries/${this.props.match.params.id}`}/>
{this.state.loading
? <Spinner />
: <div className={classes.root}>
<ViewImageDialog open={this.state.attachmentDialogOpen}
handleRequestClose={this.handleAttachmentDialogClose}
attachment={this.state.currentEntry.currentUpload}
setMarker={this.setMarker}
markerPosition={this.state.markerPosition || this.state.selectedMarkedImage && this.state.selectedMarkedImage.position}
saveMarkedImage={this.saveMarkedImage}
markedImageLoaded={this.state.markedImageLoaded}
handleMarkedImageLoaded={this.handleMarkedImageLoaded}
otherMarkedEntries={this.state.otherMarkedEntries}
/>
<CreateEntryForm handleInputChange={this.handleInputChange}
handleSubmit={this.handleSubmit}
availableProducts={filteredProducts}
addSelectedChip={this.addSelectedChip}
handleRequestDeleteChip={this.handleRequestDeleteChip}
job={this.state.job}
availableAttachments={this.state.availableAttachments}
uploadLoading={this.state.uploadLoading}
handleAttachmentDialogOpen={this.handleAttachmentDialogOpen}
markedImageLoaded={this.state.markedImageLoaded}
handleMarkedImageLoaded={this.handleMarkedImageLoaded}
isEditing={this.state.isEditing}
handleProgress={this.handleProgress}
handleUploadError={this.handleUploadError}
handleUploadSuccess={this.handleUploadSuccess}
firebaseStorage={firebase.storage().ref('images')}
filename={file => generateFilename(file)}
otherMarkedEntries={this.state.otherMarkedEntries}
{...this.state.currentEntry}
{...this.state.formErrors}
/>
{this.state.redirect && <Redirect to={redirectRoute} push />}
</div>}
</section>
);
}
}
export default withStyles(styles)(CreateEntry);
A centralised global state is a good pattern for state that needs to be global to the whole application. For me, https://redux.js.org/ is the best state engine for react applications.
When I build react/redux applications, I tend to start storing state at the lowest component level I can, and then move it up the component tree and finally into global redux state as and when it is required.
For example, a piece of state that stores whether a div is being hovered over could be stored at component level because it doesn't affect other components, but a piece of state that stores whether a modal is open might need to be in global redux state, because other parts of the application would need to know this.
I would really recommend trying out redux, or at least reading the docs.

Categories

Resources