I have a parent ponent and a child ponent. The child ponent initially renders data into a form but upon changing the data, the child ponent does not update.
Parent Component:
import React from 'react'
import { connect } from 'react-redux'
import styles from '../styles'
import ExpressionsForm from './expressionsForm'
class EditCondition extends React.Component {
constructor (props) {
super(props)
this.state = {
condition: null
}
this.updateExpression = this.updateExpression.bind(this)
this.changes = false
}
ponentWillMount () {
let conditionid = this.props.data.id
let condition = this.props.conditions.find(c => {
return (c.id = conditionid)
})
this.setState({ condition })
}
updateExpression (e) {
let expressionid = e.currentTarget.dataset.expressionid
let field = e.currentTarget.dataset.field
let value = e.target.value
let condition = this.state.condition
let expression = condition.expressions[expressionid]
expression[field] = value
condition.expressions[expressionid] = expression
this.changes = true
this.setState({ condition })
console.log('updateExpression condition: ', condition)
}
render () {
let condition = this.state.condition
if (!this.state.condition) {
return (
<div>
The selected condition with ID "{this.props.data.id}" did not load. It
may not exist. Refresh and try again.
</div>
)
}
let groupOptions = this.props.gambitGroups.map(g => {
return (
<option value={g.id} key={'group' + g.id}>
{g.name}
</option>
)
})
console.log('RENDER editCondition: ', condition) // <-- Note: This always logs as expected
let expressionsJSX = condition.expressions.map((expression, i) => {
expression.id = i
console.log('expression: ', expression) // <-- Note: This always logs as expected
return (
<ExpressionsForm
key={'expressionsForm_' + i}
expression={expression}
deleteExpression={this.deleteExpression}
updateExpression={this.updateExpression}
updateExpressionData={this.updateExpressionData}
/>
)
})
return (
<table>
<thead>
<tr>
<th {...styles.modal.tableHeaderLeftAlign}>
Device & Data Point
</th>
<th {...styles.modal.tableHeaderLeftAlign}>Operator</th>
<th {...styles.modal.tableHeaderLeftAlign}>Value</th>
<th {...styles.modal.tableHeaderLeftAlign}>PlateValue</th>
<th {...styles.modal.tableHeaderLeftAlign}> </th>
</tr>
</thead>
<tbody>{expressionsJSX}</tbody>
</table>
)
}
}
export default connect(
(state, ownProps) => ({
user: state.user,
users: state.users,
gambitGroups: state.gambitGroups,
// deviceGroups: state.deviceGroups,
conditions: state.conditions,
reactions: state.reactions,
setEditMode: ownProps.setEditMode,
navByName: ownProps.navByName
}),
dispatch => ({
addImage: file => dispatch({ type: 'UPDATE_CONDITION_LOGO', file }),
updateCondition: condition =>
dispatch({ type: 'UPDATE_CONDITION', condition })
})
)(EditCondition)
And Child Component:
import React from 'react'
import { connect } from 'react-redux'
import styles from '../styles'
class ExpressionsForm extends React.Component {
constructor (props) {
super(props)
this.state = {}
this.updateExpression = this.updateExpression.bind(this)
}
updateExpression (e) {
this.props.updateExpression(e)
}
render () {
let expression = this.props.expression
console.log('expression: ', expression) // Note: logs initial render only.
let data = expression.data
let deviceId = data.deviceId
let dataPointIndex = data.dataPointIndex
let operator = expression.operator
let plateValue = expression.plateValue
let value = expression.value
console.log('RENDER expressionForm: ', expression) // Note: logs initial render only
let deviceOptions = this.props.devices.map((device, i) => {
return (
<option value={device.id} key={'device_' + i}>
{device.userAssignedName}
</option>
)
})
let dataPointOptions = this.props.devices[0].inputs.map((input, i) => {
return (
<option value={input.id} key={'input_' + i}>
{input.name} currentValue: {input.value}
</option>
)
})
let operatorOptions = ['==', '!=', '<=', '>=', '<', '>'].map(
(operator, i) => {
return (
<option value={operator} key={'operator_' + i}>
{operator}
</option>
)
}
)
return (
<tr>
<td>
<select
{...styles.modal.inputSexy}
style={{ marginBottom: '20px' }}
data-field='deviceid'
data-expressionid={expression.id}
value={deviceId}
onChange={this.updateExpressionData}
>
<option value=''></option>
{deviceOptions}
</select>
<select
{...styles.modal.inputSexy}
data-field='dataPointIndex'
data-expressionid={expression.id}
value={dataPointIndex}
onChange={this.updateExpressionData}
>
<option value=''></option>
{dataPointOptions}
</select>
</td>
<td>
<select
{...styles.modal.inputSexy}
style={{ width: '75px' }}
data-field='operator'
data-expressionid={expression.id}
value={operator}
onChange={this.updateExpression}
>
<option value=''></option>
{operatorOptions}
</select>
</td>
<td>
<input
{...styles.modal.inputSexy}
style={{ width: '50px' }}
data-field='value'
data-expressionid={expression.id}
value={value}
onChange={this.updateExpression}
/>
</td>
<td>
<input
{...styles.modal.inputSexy}
style={{ width: '88px' }}
data-expressionid={expression.id}
data-field='plateValue'
value={plateValue}
onChange={this.updateExpression}
/>
</td>
<td>
<i className='fa fa-close'
data-expressionid={expression.id}
onClick={this.deleteExpression}
></i>
</td>
</tr>
)
}
}
export default connect(
(state, ownProps) => ({
user: state.user,
users: state.users,
devices: state.devices,
gambitGroups: state.gambitGroups,
// deviceGroups: state.deviceGroups,
conditions: state.conditions,
reactions: state.reactions,
setEditMode: ownProps.setEditMode,
navByName: ownProps.navByName
}),
dispatch => ({
addImage: file => dispatch({ type: 'UPDATE_XXX', file })
})
)(ExpressionsForm)
I have an array of objects in the redux store called Conditions. The parent ponent gets an ID of one of these conditions, finds the correct condition, and loads it into state via ponentWillMount to be modified by the user. The condition has an array of objects on it called expressions. Each of these expressions are passed to the child ponent called ExpressionsForm.
so we loop over the expressions via the map function and return the resulting JSX as expressionsJSX.
let expressionsJSX = condition.expressions.map((expression, i) => {
expression.id = i
console.log('expression: ', expression) // <-- Note: This always logs as expected
return (
<ExpressionsForm
key={'expressionsForm_' + i}
expression={expression}
deleteExpression={this.deleteExpression}
updateExpression={this.updateExpression}
updateExpressionData={this.updateExpressionData}
/>
)
})
Note that the has the expression passed to it expression={expression}
And in the child ponent's render you see
let expression = this.props.expression
console.log('expression: ', expression) // Note: logs initial render only.
Since this is a prop, whether it's being console.log'd or rendered into some JSX doesn't matter - when the prop changes the changes should also be re-rendered. BUT it's not doing it in this case. Why?
For example, I have 1 expression saved on 1 condition. It renders, I click into the plateValue input field of the expression - which contains a 5 by default - and attempt to add a 6 after the 5. When the parent ponent updates state an re-renders I see the in the console.log's that the expression's plateValue field now contains a '56'...it just doesn't render in the child ponent....!?
Here is an example console.log
Initial Render:
RENDER editCondition: {id: "1", group: 1, name: "Temperature >= 75F", meta: "If >= 75F in greenhouse turn on AC until 5 degrees cooler than 75F", expressions: Array(1)} editCondition.jsx:191 expression: {data: {…}, operator: ">=", value: "75", plateValue: "5", id: 0} expressionsForm.jsx:39 RENDER expressionForm: {data: {…}, operator: ">=", value: "75", plateValue: "5", id: 0}
Click into plateValue field and add a '6', parent rerenders...and:
editCondition.jsx:188 RENDER editCondition: {id: "1", group: 1, name: "Temperature >= 75F", meta: "If >= 75F in greenhouse turn on AC until 5 degrees cooler than 75F", expressions: Array(1)} editCondition.jsx:191 expression: {data: {…}, operator: ">=", value: "75", plateValue: "56", id: 0} editCondition.jsx:153 STATE SET! updateExpression condition: {id: "1", group: 1, name: "Temperature >= 75F", meta: "If >= 75F in greenhouse turn on AC until 5 degrees cooler than 75F", expressions: Array(1)}
I see a 'plateValue: "56"' in there. So why isn't it rerendering in the child ponent? So confused.
I've tried ponentWillReceiveProps, ponentWillUpdate, et al. I can't even get these to fire off a console.log.
Something is going on that I can't figure out. I've been doing React for a long time and I'm stumped. That doesn't happen very often anymore.
Thanks in advance for your help
PS I did look at getDerivedStateFromProps - It's great that the documentation provides examples, but they don't explain what the props and state parameters actually are. The docs suck. Their explanation sucks. Their example doesn't illustrate what it actually does. I only ever use ponentWillReceiveProps to know when a prop has changed, and then update state or whatever. getDerivedStateFromProps just confuses me. None the less I played around with it and couldn't get it to work either.
I have a parent ponent and a child ponent. The child ponent initially renders data into a form but upon changing the data, the child ponent does not update.
Parent Component:
import React from 'react'
import { connect } from 'react-redux'
import styles from '../styles'
import ExpressionsForm from './expressionsForm'
class EditCondition extends React.Component {
constructor (props) {
super(props)
this.state = {
condition: null
}
this.updateExpression = this.updateExpression.bind(this)
this.changes = false
}
ponentWillMount () {
let conditionid = this.props.data.id
let condition = this.props.conditions.find(c => {
return (c.id = conditionid)
})
this.setState({ condition })
}
updateExpression (e) {
let expressionid = e.currentTarget.dataset.expressionid
let field = e.currentTarget.dataset.field
let value = e.target.value
let condition = this.state.condition
let expression = condition.expressions[expressionid]
expression[field] = value
condition.expressions[expressionid] = expression
this.changes = true
this.setState({ condition })
console.log('updateExpression condition: ', condition)
}
render () {
let condition = this.state.condition
if (!this.state.condition) {
return (
<div>
The selected condition with ID "{this.props.data.id}" did not load. It
may not exist. Refresh and try again.
</div>
)
}
let groupOptions = this.props.gambitGroups.map(g => {
return (
<option value={g.id} key={'group' + g.id}>
{g.name}
</option>
)
})
console.log('RENDER editCondition: ', condition) // <-- Note: This always logs as expected
let expressionsJSX = condition.expressions.map((expression, i) => {
expression.id = i
console.log('expression: ', expression) // <-- Note: This always logs as expected
return (
<ExpressionsForm
key={'expressionsForm_' + i}
expression={expression}
deleteExpression={this.deleteExpression}
updateExpression={this.updateExpression}
updateExpressionData={this.updateExpressionData}
/>
)
})
return (
<table>
<thead>
<tr>
<th {...styles.modal.tableHeaderLeftAlign}>
Device & Data Point
</th>
<th {...styles.modal.tableHeaderLeftAlign}>Operator</th>
<th {...styles.modal.tableHeaderLeftAlign}>Value</th>
<th {...styles.modal.tableHeaderLeftAlign}>PlateValue</th>
<th {...styles.modal.tableHeaderLeftAlign}> </th>
</tr>
</thead>
<tbody>{expressionsJSX}</tbody>
</table>
)
}
}
export default connect(
(state, ownProps) => ({
user: state.user,
users: state.users,
gambitGroups: state.gambitGroups,
// deviceGroups: state.deviceGroups,
conditions: state.conditions,
reactions: state.reactions,
setEditMode: ownProps.setEditMode,
navByName: ownProps.navByName
}),
dispatch => ({
addImage: file => dispatch({ type: 'UPDATE_CONDITION_LOGO', file }),
updateCondition: condition =>
dispatch({ type: 'UPDATE_CONDITION', condition })
})
)(EditCondition)
And Child Component:
import React from 'react'
import { connect } from 'react-redux'
import styles from '../styles'
class ExpressionsForm extends React.Component {
constructor (props) {
super(props)
this.state = {}
this.updateExpression = this.updateExpression.bind(this)
}
updateExpression (e) {
this.props.updateExpression(e)
}
render () {
let expression = this.props.expression
console.log('expression: ', expression) // Note: logs initial render only.
let data = expression.data
let deviceId = data.deviceId
let dataPointIndex = data.dataPointIndex
let operator = expression.operator
let plateValue = expression.plateValue
let value = expression.value
console.log('RENDER expressionForm: ', expression) // Note: logs initial render only
let deviceOptions = this.props.devices.map((device, i) => {
return (
<option value={device.id} key={'device_' + i}>
{device.userAssignedName}
</option>
)
})
let dataPointOptions = this.props.devices[0].inputs.map((input, i) => {
return (
<option value={input.id} key={'input_' + i}>
{input.name} currentValue: {input.value}
</option>
)
})
let operatorOptions = ['==', '!=', '<=', '>=', '<', '>'].map(
(operator, i) => {
return (
<option value={operator} key={'operator_' + i}>
{operator}
</option>
)
}
)
return (
<tr>
<td>
<select
{...styles.modal.inputSexy}
style={{ marginBottom: '20px' }}
data-field='deviceid'
data-expressionid={expression.id}
value={deviceId}
onChange={this.updateExpressionData}
>
<option value=''></option>
{deviceOptions}
</select>
<select
{...styles.modal.inputSexy}
data-field='dataPointIndex'
data-expressionid={expression.id}
value={dataPointIndex}
onChange={this.updateExpressionData}
>
<option value=''></option>
{dataPointOptions}
</select>
</td>
<td>
<select
{...styles.modal.inputSexy}
style={{ width: '75px' }}
data-field='operator'
data-expressionid={expression.id}
value={operator}
onChange={this.updateExpression}
>
<option value=''></option>
{operatorOptions}
</select>
</td>
<td>
<input
{...styles.modal.inputSexy}
style={{ width: '50px' }}
data-field='value'
data-expressionid={expression.id}
value={value}
onChange={this.updateExpression}
/>
</td>
<td>
<input
{...styles.modal.inputSexy}
style={{ width: '88px' }}
data-expressionid={expression.id}
data-field='plateValue'
value={plateValue}
onChange={this.updateExpression}
/>
</td>
<td>
<i className='fa fa-close'
data-expressionid={expression.id}
onClick={this.deleteExpression}
></i>
</td>
</tr>
)
}
}
export default connect(
(state, ownProps) => ({
user: state.user,
users: state.users,
devices: state.devices,
gambitGroups: state.gambitGroups,
// deviceGroups: state.deviceGroups,
conditions: state.conditions,
reactions: state.reactions,
setEditMode: ownProps.setEditMode,
navByName: ownProps.navByName
}),
dispatch => ({
addImage: file => dispatch({ type: 'UPDATE_XXX', file })
})
)(ExpressionsForm)
I have an array of objects in the redux store called Conditions. The parent ponent gets an ID of one of these conditions, finds the correct condition, and loads it into state via ponentWillMount to be modified by the user. The condition has an array of objects on it called expressions. Each of these expressions are passed to the child ponent called ExpressionsForm.
so we loop over the expressions via the map function and return the resulting JSX as expressionsJSX.
let expressionsJSX = condition.expressions.map((expression, i) => {
expression.id = i
console.log('expression: ', expression) // <-- Note: This always logs as expected
return (
<ExpressionsForm
key={'expressionsForm_' + i}
expression={expression}
deleteExpression={this.deleteExpression}
updateExpression={this.updateExpression}
updateExpressionData={this.updateExpressionData}
/>
)
})
Note that the has the expression passed to it expression={expression}
And in the child ponent's render you see
let expression = this.props.expression
console.log('expression: ', expression) // Note: logs initial render only.
Since this is a prop, whether it's being console.log'd or rendered into some JSX doesn't matter - when the prop changes the changes should also be re-rendered. BUT it's not doing it in this case. Why?
For example, I have 1 expression saved on 1 condition. It renders, I click into the plateValue input field of the expression - which contains a 5 by default - and attempt to add a 6 after the 5. When the parent ponent updates state an re-renders I see the in the console.log's that the expression's plateValue field now contains a '56'...it just doesn't render in the child ponent....!?
Here is an example console.log
Initial Render:
RENDER editCondition: {id: "1", group: 1, name: "Temperature >= 75F", meta: "If >= 75F in greenhouse turn on AC until 5 degrees cooler than 75F", expressions: Array(1)} editCondition.jsx:191 expression: {data: {…}, operator: ">=", value: "75", plateValue: "5", id: 0} expressionsForm.jsx:39 RENDER expressionForm: {data: {…}, operator: ">=", value: "75", plateValue: "5", id: 0}
Click into plateValue field and add a '6', parent rerenders...and:
editCondition.jsx:188 RENDER editCondition: {id: "1", group: 1, name: "Temperature >= 75F", meta: "If >= 75F in greenhouse turn on AC until 5 degrees cooler than 75F", expressions: Array(1)} editCondition.jsx:191 expression: {data: {…}, operator: ">=", value: "75", plateValue: "56", id: 0} editCondition.jsx:153 STATE SET! updateExpression condition: {id: "1", group: 1, name: "Temperature >= 75F", meta: "If >= 75F in greenhouse turn on AC until 5 degrees cooler than 75F", expressions: Array(1)}
I see a 'plateValue: "56"' in there. So why isn't it rerendering in the child ponent? So confused.
I've tried ponentWillReceiveProps, ponentWillUpdate, et al. I can't even get these to fire off a console.log.
Something is going on that I can't figure out. I've been doing React for a long time and I'm stumped. That doesn't happen very often anymore.
Thanks in advance for your help
PS I did look at getDerivedStateFromProps - It's great that the documentation provides examples, but they don't explain what the props and state parameters actually are. The docs suck. Their explanation sucks. Their example doesn't illustrate what it actually does. I only ever use ponentWillReceiveProps to know when a prop has changed, and then update state or whatever. getDerivedStateFromProps just confuses me. None the less I played around with it and couldn't get it to work either.
Share Improve this question edited Jun 16, 2021 at 7:53 John asked Jun 16, 2021 at 7:25 JohnJohn 1,0802 gold badges16 silver badges23 bronze badges 2-
The only thing that seems to work is by doing a
let ex = Object.assign({},expression)
and passingex
as the value to the expression prop. Why does this seem to work? – John Commented Jun 16, 2021 at 8:17 - 1 can you make a minimal example focusing to the problematic code parts only? – gazdagergo Commented Jun 16, 2021 at 9:41
1 Answer
Reset to default 6It looks like the same expression
object is being passed in all the time.
React checks the props
that a ponent receives for changes when deciding to render. It finds that none of the props
items have changed, they are all the same objects as before, and concludes that the child ponent does not need to be rerendered. It will not do a deep inspection of all properties of each prop.
This also explains why a rerender can be forced by making a copy of the expression object. The copy is always a new object, thus causing a rerender, regardless if any of its content have changed or not.
You could avoid this situation as you do already, by making a copy, or by dissecting the expression
object into its properties and then feeding each of those as separate props
into the child.
As a final note, a copy can also be made by passing it in as expression={{...expression}}
.