0

I have a Javascript complex data structure with 2 person fields - customer and payer (both are of type Person)

{
    invoice: {
        id: 123,
        warehouseId: 456;
        customer: {
            id: 777,
            name: "Coco"
        }
        payer: {
            id: 778,
            name: "Roro"
        }
    }
}

I am using child component for displaying Person object:

class ConnectedPersonFieldSet extends Component {
    render () {
        return 
            <div>
                <div>{this.props.label}</div>
                <div>{this.props.data.id}</div>
                <div>{this.props.data.name}</div>
            </div>
    }
}
const PersonFieldSet = connect(mapStateToProps, mapDispatchToProps)(ConnectedPersonFieldSet);
export default PersonFieldSet;

And I have parent component that display full Invoice object and which has 2 child components for customer and payer respectively:

class ConnectedInvoice extends Component {
    render () {
        return
            <div>
                <div>{this.props.invoice.id}</div>
                <div>{this.props.invoice.warehouseId}</div>
                <PersonFieldSet label={"Customer" + /* this.props.customer.name */ } data={this.props.customer}></PersonFieldSet>
                <PersonFieldSet label="Payer" data={this.props.payer}></PersonFieldSet>
            </div>
    }
}
const Invoice = connect(mapStateToProps, mapDispatchToProps)(ConnectedInvoice);
export default Invoice;

I have also complex logic that changes just invoice.customer.name. The updated customer name becomes visible in the Invoice component:

<div>{this.props.invoice.id}</div>

But, unfortunately, the

<PersonFieldSet label={"Customer" + /* this.props.customer.name */ } data={this.props.customer}></PersonFieldSet>

stays the same. If I uncomment /* this.props.customer.name */ then the updated customer.name becomes visible both in the label and in the name subcomponent of the PersonFieldSet.

So - my question is - why the child component, which receives the object, can not detect the change of the one attribute of this object and hence, does not update visual data upon the change of the one attribute of the object?

If the child component is able to feel somehow (e.g. via label={"..." + this.props.customer.name}) that the update of the attribute happened, then the child component displays the full update of all the attributes.

How to press the child component to detect that attributes can change the forwarded object?

I have read (e.g. React: why child component doesn't update when prop changes) that there is a trick with (more or less redundant) key attribute of the child element, but is this really my case?

My understanding is that React should support the hierarchical composition of both visual components and data components and do it without tricks or any other intrigues, but how to handle my situation? Should I really start to use hacks (key or others) in this situation that is pretty standard?

Added: I am using Redux architecture for making updates - currently I am testing update of just one field - name:

const rootReducer = (state = initialState, action) => {
    switch(action.type) {
        case UPDATE_INVOICE_CUSTOMER: {
              let person_id = action.payload.person_id;
             
              let data = {
                invoice: state.invoice
              }
              
              let newData = updateInvoiceByCustomer(data, person_id);

              return {...state,
                  invoice: newData.invoice,
              }
            }
    }
}

export function updateInvoiceByCustomer(data, person_id) {
  let newData = {
    invoice: data.invoice,
  }

  /* This will be replaced by the complex business logic, that retrieves
     customer from the database using person_id and afterwards complex
     calculations are done on the invoice, e.g. discounts and taxes
     are assigned according to the rules relevant for the specific 
     customer. Possible all this code will have to be moved to the chain
     of promises */
  newData.invoice.customer.name='Test';

  return newData;
}
Sanket Shah
  • 2,888
  • 1
  • 11
  • 22
TomR
  • 2,696
  • 6
  • 34
  • 87
  • 2
    _"I have also complex logic that changes just `invoice.customer.name`..."_ = **how**, though? If you do it by mutating the existing object, React's change detection cannot see that. – jonrsharpe Aug 10 '21 at 10:43
  • @jonrsharpe I added Redux code which explains how my updates are done. – TomR Aug 10 '21 at 10:51
  • 1
    Your reducer is a *bit awkward*, as in its overly complicated yet doesn't actually copy all the relevant objects, `lkPerson` is unused, `newData.customer` is undefined... If this is all your code you should actually get some errors in the console. If this is not all your code, then the line `newData.customer.name='Test';` just *smells* of mutation. Which would then explain the observed behavior (react to rerendering). – Yoshi Aug 10 '21 at 11:06
  • I corrected - newData.customer.name=..., of course it had to be newData.invoice.customer.name. Regarding mutation - as I understand - it is perfectly legal to do mutations on the newData variable/object. After the newData is computed (sequentially, incrementaally), the newData can be fused into state with one operation as my code already suggests. – TomR Aug 10 '21 at 11:28
  • 1
    Regarding *"...perfectly legal to do mutations on the newData..."*, true, if it actually were *new data*, as in actually deeply cloned. Which is not the case in your code. From what is shown, your're working directly on `state.invoice`. You're never actually cloning that object. And as such, it's not recognized as *new*. If you don't want to bother with deeply cloning the state over and over again (as it can get very verbose), I'd suggest using a library. [Immer](https://immerjs.github.io/immer/) is an excellent choice (IMO). – Yoshi Aug 10 '21 at 11:33
  • One short addition. To be perfectly clear, so that there are no misconceptions: even if you only want to change some very minor datum inside some deeply nested state, you absolutely must create a completely new state! – Yoshi Aug 10 '21 at 11:53

1 Answers1

1

Thanks @Yoshi for comments on my question and for persisting to check my Redux update logic. Indeed, when I have removed all the copying-update logic (which should be corrected to use cloning) and replaced it by:

         return {
            ...state,
            ['invoice']: {...state['invoice'],
              ['customer']: {...state['invoice']['customer'],
                ['name']: 'REAL-TEST',
              }
            }
          }

Then child component started to re-render and to show the actual value without any hacks or use of key-attributes. So, that was the cause of error.

TomR
  • 2,696
  • 6
  • 34
  • 87