Bloated state in React - javascript

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.

Related

How can I 'not allow' the user to retrieve same information from the server multiple times in my 'React' Weather-app?

I'm learning React and as a practice I'm building a weather-app.
My App Photo
Everything is ok. but I want When a User type a City name for example 'London' and click the Search button and get the data . the next search if it's 'london' (which is currently visible) i don't want my handleGetData function to call server for data.
How should implement this logic ? What should i change? I will appreciate your help
import React, { Component } from "react";
import InputSearch from "./common/inputSearch";
import http from "../services/httpService";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import Weather from "./weather";
class App extends Component {
state = {
city: {
name: "",
info: [],
},
};
handleChange = (e) => {
const city = this.state.city;
city.name = e.currentTarget.value;
this.setState({ city });
};
handleGetData = async () => {
try {
const city = this.state.city;
const apiEndPoint = `http://api.weatherapi.com/v1/forecast.json?key=97de37820e0e4a29b9c90051221604&q=${city.name}&days=3&aqi=no&alerts=no`;
const { data } = await http.get(apiEndPoint);
city.info = data;
this.setState({ city });
} catch (e) {
const expectedError =
e.response.status && e.response.status > 400 && e.response.status < 500;
if (expectedError) {
toast.error("Something failed while getting Data");
}
}
};
render() {
const { city } = this.state;
return (
<React.Fragment>
<ToastContainer />
<InputSearch
stateCityName={city.name}
onChanges={this.handleChange}
onGettingData={this.handleGetData}
/>
<Weather data={city.info} data2={city} />
</React.Fragment>
);
}
}
export default App;
You can use a new state as your previousSearch and compare it with your current search
It's something like this but I didn't test it though so you may have to do some modification
state = {
city: {
name: "",
info: [],
},
previousSearch: "",
};
handleChange = (e) => {
const city = this.state.city;
prevCity = city.name;
city.name = e.currentTarget.value;
this.setState({ city, previousSearch: prevCity });
};
handleGetData = async () => {
try {
const city = this.state.city;
if(previousSearch !== city.name) {
const apiEndPoint = `http://api.weatherapi.com/v1/forecast.json?key=97de37820e0e4a29b9c90051221604&q=${city.name}&days=3&aqi=no&alerts=no`;
const { data } = await http.get(apiEndPoint);
city.info = data;
this.setState({ city });
}
} catch (e) {
const expectedError =
e.response.status && e.response.status > 400 && e.response.status < 500;
if (expectedError) {
toast.error("Something failed while getting Data");
}
}
};
Have fun

Adding form data and rendering to the DOM React

I am creating warehouse management application where I enter data to the form in the form component. When submitting form I want to render entered data to the DOM, in the local storage: http://localhost:3000/products/. changeHandler works good, I receive data from input fields. However, addItemHandler does not work and it does not render anything. Could anybody help me, please.
import React, { Component, createContext } from "react";
import nextId from "react-id-generator";
const ProductContext = createContext();
class ProductProvider extends Component {
newId = nextId();
state = {
products: [
{ id: "", name: "", ean: "", type: "", weight: "", color: "", quantity: "", price: "", info: "", }, ],
detailProduct: "",
};
componentDidMount() {
this.setProducts();
}
setProducts = () => {
let products = [];
this.state.products.forEach((item) => {
const singleItem = { ...item };
products = [...products, singleItem];
});
this.setState(() => {
return { products: products };
});
};
getItem = (id) => {
const product = this.state.products.find((item) => item.id === id);
return product;
};
productDetailHandler = (id) => {
const product = this.getItem(id);
this.setState(() => {
return { detailProduct: product };
});
};
changeHandler = (event) => {
const value = event.target.value;
this.setState({
products: { ...this.state.products, [event.target.name]: value },
});
};
addItemHandler = ( event, name, ean, type, weight, color, quantity, price, info ) => {
event.preventDefault();
const products = [
...this.state.products,
{ name, ean, type, weight, color, quantity, price, info, id: this.newID },
];
this.setState({ products: products });
console.log(this.state);
};
render() {
return (
<ProductContext.Provider
value={{
...this.state,
productDetailHandler: this.productDetailHandler,
changeHandler: this.changeHandler,
addItemHandler: this.addItemHandler,
}}
>
{this.props.children}
</ProductContext.Provider>
);
}
}
const ProductConsumer = ProductContext.Consumer;
export { ProductProvider, ProductConsumer };
state = {
...
newProductItem: {}
};
// the `products`'s type is an Object Array
changeHandler = (event) => {
const value = event.target.value;
const {newProductItem} = this.state
this.setState({
newProductItem: { ...newProductItem, [event.target.name]: value}
});
}
addItemHandler = (event) => {
event.preventDefault();
const {newProductItem} = this.state
const products = [
...this.state.products,
{...newProductItem, id:this.newID}
];
this.setState({
products,
newProductItem: {}
});
};
==== in diff page ==
// form.jsx
state = {
...
newProductItem: {}
};
...
changeHandler = (event) => {
const value = event.target.value;
const { newProductItem } = this.state
this.setState({
newProductItem: { ...newProductItem, [event.target.name]: value}
});
}
...
render(){
...
const { addItemHandler} = this.props
const { newProductItem } = this.state
<Form submit={(ev)=> {
addItemHandler(ev, newProductItem);
this.setState({newProductItem:{}});
}>
<Input onChange={changeHandler} />
</Form>
}
...
<>
...
// main.jsx
addItemHandler = (event, newProductItem) => {
event.preventDefault();
const products = [
...this.state.products,
{...newProductItem, id:this.newID}
];
this.setState({
products
});
};

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

Prop getting lost in unusual situation

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.

Categories

Resources