Background
I am working on a Meteor app that uses ReactJS as the rendering library.
Currently, I'm having trouble with having a Child component re-render when data has been updated, even though the parent is accessing the updated data & supposedly passing it down to the child.
The Parent component is a Table of Data. The Child component is a Click-to-edit date field.
The way it (theoretically) works: Parent component passes the existing data for date into child component as a prop. Child component takes that existing props data, handles it & sets some states using it and then has 2 options:
I am using the child component twice per row in the table, and each time it is used, it needs access to the most current date data from the database. So if it the data is changed in one field, the second field should reflect that change.
Problem
The second field is not reflecting changes in the database unless I manually refresh the page and force the child component to render with the new data. The edited field is reflecting the data change, because it's reflecting what's stored in state.
After reading React documentation, I am sure the problem is because the date is coming in as a prop, then being handled as a state--and because the component isn't going to re-render from a prop change.
My Question
What do I do to fix this?
All of the reading I've done of the docs has strongly recommended staying away from things like forceUpdate() and getDerivedStateFromProps(), but as a result I'm not sure how to have data pass through the way that I want it to.
Thoughts?
My Code
I've abbreviated the code a little bit & removed variable names specific to my project, but I can provide more of the actual if it'll help. I think my question is more conceptual than it is straight up debugging.
Parent
ParentComponent () {
//Date comes as a prop from database subscription
var date1 = this.props.dates.date1
var date2 = this.props.dates.date2
return(
<ChildComponent
id='handle-date-1'
selected={[date1, date2]} />
<ChildComponent
id='handle-date-2'
selected={[date1, date2]} />
)
}
Child
ChildComponent() {
constructor(props) {
super(props);
this.state = {
date1: this.props.selected[0],
date2: this.props.selected[1],
field: false,
};
}
handleDatePick() {
//Handles the event listeners for clicks in/out of the div, handles calling Meteor to update database.
}
renderDate1() {
return(
<div>
{this.state.field == false &&
<p onClick={this.handleClick}>{formatDate(this.state.date1)}</p>}
{this.state.field == true &&
<DatePicker
selected={this.state.date1}
startDate={this.state.date1}
onChange={this.handleDatePick}
/>
}
</div>
)
}
renderDate2() {
return(
<div>
{this.state.field == false &&
<p onClick={this.handleClick}>{formatDate(this.state.date2)}</p>}
{this.state.field == true &&
<DatePicker
selected={this.state.date2}
startDate={this.state.date2}
onChange={this.handleDatePick}
/>
}
</div>
)
}
render() {
return(
//conditionally calls renderDate1 OR renderDate2
)
}
}
(If this code/my explanation is rough, it's because I'm still a fairly beginner/low level developer. I don't have formal training, so I'm learning on the job while working on a pretty difficult app. As a solo developer. It's a long story. Please be gentle!)
1. Memoization using useMemo() and UseCallback() Hooks. Memoization enables your code to re-render components only if there's a change in the props. With this technique, developers can avoid unnecessary renderings and reduce the computational load in applications.
Triggering a Child Component to Re-render To force the child component to re-render — and make a new API call — we'll need to pass a prop that will change if the user's color preference has changed. This is a simple switch we can flip.
If you don't want a component to re-render when its parent renders, wrap it with memo. After that, the component indeed will only re-render when its props change.
If you're using a React class component you can use the shouldComponentUpdate method or a React. PureComponent class extension to prevent a component from re-rendering.
The React docs have a section on best practice usage of constructor()
. Reading that, paying close attention to the yellow-highlighted "Note" section, should illuminate the exact problem you're running into.
Essentially, constructor()
is only run once, and is most commonly used to initialize internal/local state (or bind methods). This means that your child component is setting date1
and date2
with the values from your selected
prop once when constructor()
is called for that child. Whatever the value of selected
is at the time constructor()
is called will be set to the child's state, and will remain the same even if the value continues to change.
Thus, any successive updates to the selected
prop passed to a child component will not be reflected in that component's internal state, meaning there will be no re-render of that component. You'll need to make use of React's setState()
method elsewhere in your child component to properly update that child's state and trigger a re-render.
Using a combination of the React lifecycle methods to properly update your child components is the way to go. The snippet below gives you the main idea of what to change about your implementation in componentDidMount()
and componentDidUpdate()
class Child extends React.Component {
constructor(props) {
super(props);
this.state = {
date1: 0,
date2: 0,
};
}
/*
Any time the child mounts to the DOM,
you can use the method below to set
your state using the current value of your
selected prop from the parent component...
*/
componentDidMount() {
this.setState({
date1: this.props.selected[0],
date2: this.props.selected[1]
});
}
/*
When the child receives an update from its parent,
in this case a new date selection, and should
re-render based on that update, use the method below
to make a comparison of your selected prop and then
call setState again...
*/
componentDidUpdate(prevProps) {
if (prevProps.selected !== this.props.selected) {
this.setState({
date1: this.props.selected[0],
date2: this.props.selected[1]
});
}
}
render() {
const { date1, date2 } = this.state;
return (
<div style={{ border: `4px dotted red`, margin: 8, padding: 4 }}>
<h1 style={{ fontWeight: 'bold' }}>Child</h1>
<h2>The value of date1 is {date1}</h2>
<h2>The value of date2 is {date2}</h2>
</div>
);
}
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
valOne: 0,
valTwo: 0
};
}
incrementOne = () => {
this.setState(prevState => ({ valOne: (prevState.valOne += 1) }));
};
incrementTwo = () => {
this.setState(prevState => ({ valTwo: (prevState.valTwo += 1) }));
};
render() {
const { valOne, valTwo } = this.state;
return (
<div style={{ border: `4px solid blue`, margin: 8, padding: 4 }}>
<h1 style={{ fontWeight: 'bold', fontSize: 18 }}>Parent</h1>
<button onClick={() => this.incrementOne()}>Increment date1</button>
<button onClick={() => this.incrementTwo()}>Increment date2</button>
<Child selected={[valOne, valTwo]} />
</div>
);
}
}
ReactDOM.render(<Parent />, document.querySelector('#app'));
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>
Since you're using the React Component pattern for your children, making good use of the React lifecyle methods will really help you out here. I can't suggest strongly enough that you learn the React component lifecyle. As you continue to use React, it will become essential in cases such as this. componentDidMount()
and componentDidUpdate()
are good places to start.
Hope that this helps. Let us know how it turns out.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With