Reputation: 29
I'm trying to build a CV generator web app and I'm having problems with properly setting states so they display correct information. The issue is I can't seem to figure out how to set states so that my CV preview renders correctly, I have tried a of different things with no effect. Any tips or solutions would be welcome.
As for the code here's a hosted Sandbox
Also code incase sandbox goes down:
App.js
import { Component } from "react";
import "./styles/App.css";
import CV from "./cv/CV";
import Form from "./form/Form";
class App extends Component {
constructor() {
super();
this.state = {
Experience: {
Experience: {
Workplace: '',
Position: '',
ToExp: '',
FromExp: '',
AchievementsExp: '',
}
},
Experiences: [
{Experience: {
Workplace: '',
Position: '',
ToExp: '',
FromExp: '',
AchievementsExp: '',
}},
],
}
};
handleChangeExperience = (e) => {
const key= e.target.name
const value = e.target.value
this.setState({
Experience: {
Experience: {
[key]: value,
},
},
});
};
handleExperiencePush = (e) => {
e.preventDefault();
this.state.Experiences.push(this.state.Experience);
this.setState({
Experiences: this.state.Experiences
})
}
handleExperiencePop = (e) => {
e.preventDefault();
this.state.Experiences.pop();
this.setState({
Experiences: this.state.Experiences
})
}
render() {
return (
<div className="App">
<Form
handleChangeExperience = {this.handleChangeExperience}
handleExperiencePush = {this.handleExperiencePush}
handleExperiencePop = {this.handleExperiencePop}
handleFormSubmit = {this.handleFormSubmit}
experiences = {this.state.Experiences}
/>
<CV
experiences = {this.state.Experiences}
/>
</div>
);
};
};
export default App;
Also for reference, here's how I try to cast each element of Experiences array into a displaying component
Component.js
import React, { Component } from "react";
import PersonalCV from "./PersonalCV";
class CV extends Component {
constructor(props) {
super(props)
}
render(){
return (
<div className="CV">
{this.props.experiences.map((Experience) => {
return (
<PersonalCV
Experience = {Experience}
/>
)
})}
</div>
)
}
}
export default CV;
Upvotes: 0
Views: 78
Reputation: 328
A couple issues here.
First, on setState
, React performs a shallow merge of the current state and new state. It only merges direct properties of the state object.
Actually I think merge is a confusing word to use here, because it suggests React will do more than it actually does. The algorithm is simple:
For clarity, I will change your state shape so each key is unique:
{
CurrentExperience: {
Experience: {
...
}
},
AllExperiences: [{ Experience: { ... } }]
}
Take a look at this:
handleChangeExperience = (e) => {
const key= e.target.name
const value = e.target.value
this.setState({
CurrentExperience: {
Experience: {
[key]: value,
},
},
});
};
Imagine our current state has:
{
CurrentExperience: {
Experience: {
Workplace: 'MegaCorp',
Position: 'Senior Developer',
ToExp: 'today',
FromExp: '2019',
AchievementsExp: 'Went to lots of meetings',
}
},
AllExperiences: [...]
}
You realize that maybe this isn't the most compelling CV entry, so you use the form to update it, sending an event to handleChangeExperience
:
{
target: {
name: "AchievementsExp",
value: "Synergized with key stakeholders to empower strategic value-adds"
}
}
Now handleChangeExperience
calls setState
:
this.setState({
CurrentExperience: {
Experience: {
AchievementsExp: "Synergized with key stakeholders to empower strategic value-adds"
}
}
})
According to the algorithm above, what happens?
Start with the current state object:
{
CurrentExperience: {
Experience: {
Workplace: "MegaCorp",
Position: "Senior Developer",
...
}
},
AllExperiences: [...]
}
Examine the update object:
{
CurrentExperience: {
Experience: {
AchievementsExp: "Synergized with key stakeholders to enable win-win value-adds"
}
}
}
For each key in the update object, if the state's value at that key is different from the update's value at that key, set the current state's value at that key to the updated value:
reactInternalState.CurrentExperience = update.CurrentExperience
What is the result?
{
CurrentExperience: {
Experience: {
AchievementsExp: "Synergized with key stakeholders to enable win-win value-adds"
}
},
AllExperiences: [ ... ]
}
A state value was changed, so we re-render.
But - uh oh, we lost all the rest of the information inside CurrentExperience.Experience
!
Instead, beyond the immediate children of the state, we need to merge objects ourselves, like this:
const oldExperience = state.CurrentExperience.Experience
const newExperience = {
...oldExperience,
[key]: value
}
this.setState({
CurrentExperience: {
Experience: newExperience
}
})
Second, React always compares props and values of state
by reference, meaning "is this the same object", not "does this object have the same values".
Consider this method:
handleExperiencePush = (e) => {
e.preventDefault();
this.state.AllExperiences.push(this.state.CurrentExperience);
this.setState({
AllExperiences: this.state.AllExperiences
})
}
state.AllExperiences
is an array. Let's call him Bob. Bob has a bag of things.
this.state.AllExperiences.push(this.state.CurrentExperience)
says "Bob, please put this.state.CurrentExperience
into your bag." Now Bob's bag has whatever it had before, and also this.state.CurrentExperience
.
Next we set the state:
this.setState({
AllExperiences: this.state.CurrentExperiences
})
React does the same thing we looked at above - it takes each key in the update object, compares its value to that key's value in the state object, and changes the state if the values are different.
So React looks at the update value, and sees that AllExperiences
is Bob the Array. Then it looks at the current state, and sees that AllExperiences
is Bob the Array. Since the two values are equal, no state change is needed. Since there was no state change, React won't re-render the component. (More precisely, React is not guaranteed to re-render the component. Whether or not React re-renders is an internal implementation detail. It can re-render whenever it wants to. All we can do is take actions that guarantee that there will eventually be a re-render with updates.)
So for React to understand that there is an update and re-render, we need to give it a different value - a different array. Like this:
handleExperiencePush = (e) => {
e.preventDefault();
const newExperiences = [
...this.state.AllExperiences,
this.state.CurrentExperience
];
this.setState({
AllExperiences: newExperiences
})
}
Here, we create a new array. Let's call her Jane.
Then we take all of the items out of Bob's bag, make a new bag, put all the items into it (in the same order), and give the bag to Jane. (Bob's bag is unchanged, though - the physical analogy has limits.) Then we say "Jane, here is this.state.CurrentExperience
, put it in your bag".
Now we call setState
. React compares the value at state.CurrentExperience
to the value at update.CurrentExperience
, and sees that while the current state has Bob the Array, the update has Jane the Array. Since these are different arrays, React kicks out Bob, puts Jane at state.CurrentExperience
, and since there was a change, it re-renders. (Again, technically: since there was a change, App
is now guaranteed to eventually re-render with the change applied. But React has the right to wait before re-rendering, or to collect several changes and re-render with all of them at once, or whatever React feels like doing.)
General advice: whenever you're in a situation where you change the state and it doesn't update as expected, or it seems like updates are delayed or inconsistent across multiple components, first make sure that whenever you update state, state actually gets new objects and arrays and never re-uses existing ones.
Upvotes: 1