最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - React 16.13.1 - child component does not re-render when props change - Stack Overflow

programmeradmin1浏览0评论

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 &amp; 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}>&nbsp;</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>
          &nbsp;
        </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 &amp; 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}>&nbsp;</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>
          &nbsp;
        </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 passing ex 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
Add a ment  | 

1 Answer 1

Reset to default 6

It 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}}.

发布评论

评论列表(0)

  1. 暂无评论