How can you use getSObjectValue() in a child component who’s property is declared with @api

lightning-web-componentsparent-to-child

Summary:

Is there a way to pass an object from a parent component to a child component so that the child component can use getSObjectValue() to read field values?

The naïve implementation fails because the child component's property is declared as @api, which exposes a Proxy object that is incompatible with getSObjectValue().

Details:

When reading values from an object returned from an Apex call, the Salesforce recommendation is to import the appropriate fields from the schema and to use getSObjectValue() to read the field values.

This is documented here:

Import Objects and Fields from @salesforce/schema

However, if you pass an object from a parent component to a child component using the exposed @api property, then you can no longer use getSObjectValue() in the child component because @api exposes a Proxy object, which getSObjectValue() cannot handle.

Is there a way to pass an object to a child component and still be able to use getSObjectValue() to read the appropriate field value? If not, is there another way to import the fields from a schema into a child component so that dependency tracking is still performed?

Consider the following parent-child example where the parent component wants to render a bunch of child component elements for each Contact returned from an Apex call:

Parent Component:

The parent component calls an Apex method imperatively to fetch a list of all Contacts:

import { LightningElement } from 'lwc';
import getAllContacts from '@salesforce/apex/getAllContacts';

export default class ContactsBrowser extends LightningElement {
  @track contacts;
  @track error;

  handleLoad() {
    getContactList()
     .then(result => {
       this.contacts = result;
     })
     .catch(error => {
       this.error = error;
     });
  }
}

Parent Template:

The parent template then renders each contact in a child component, passing in the appropriate Contact through the child's @api property:

<template>
  <template lwc:if={contacts}>
    <template for:each={contacts} for:item="contact">
      <c-contact-row key={contact.Id} contact={contact}></c-contact-row>
    </template>
  </template>
</template>

Child Component:

The child component exposes @api contact and uses getSObjectValue() to read a field value from the given object. However, this will fail because getSObjectValue() expects a normal JavaScript object but contact is actually a Proxy object.

Using contact.Name works fine, but then we're no longer taking advantage of the imported field and potentially losing some dependency tracking.

import { LightningElement, api } from 'lwc';
import { getSObjectValue } from '@salesforce/apex';
import NAME_FIELD from '@salesforce/schema/Contact.Name';

export default class ContactRow extends LightningElement {
  @api contact;

  get name() {
    // This fails because `contact` is a Proxy object.
    return getSObjectValue(this.contact, NAME_FIELD);
  }
}

Edits:

In an attempt to create a simple example, the initial version of this post used the @wire service instead of calling an Apex method imperatively, like the real code does. This mistake results in very different answers. Apologies for the confusion.

Best Answer

As I've said a few times before, Proxy is almost certainly not the problem here. Proxy is nearly perfectly transparent to code running live, it just so happens that console.log outputs the Proxy instead of its [[Target]], which is what the Proxy is trapping.

It's more likely that you're running into specific conditions because you're using a getter. Depending on the circumstances, the LWR (Lightning Web Runtime) may not realize that you've changed the value, and therefore skips checking the getters. It is far more reliable in this case to set the value from the @api property itself:

name;
#contact; // # denotes private variable
@api set contact(value) {
  this.name = getSObjectValue(value, NAME_FIELD);
  this.#contact = value;
}
get contact() {
  return this.#contact;
}

Doing this should improve the reactivity of the variable, but note that you need to make sure that you don't try to modify this.#contact, as it is still read-only unless/until you copy it. If you never need to get the value, you may omit the getter.

Related Topic