Moving from Aura to LWC, stumbling blocks on the road to success

Published in Articles

1. Objects passed to child components are read-only

Child components cannot change the content of the non-primitive properties that passed thought attributes. The following error will be displayed when the child component tries to change the content of such properties:

['set' on proxy: trap returned falsish for property 'name']

The next example will help you to reproduce this situation:
<!-- parentCmp.html -->
<template>
    <c-child-cmp record={account}></c-child-cmp>
</template>
// parentCmp.js
import { LightningElement } from 'lwc';
export default class ParentCmp extends LightningElement {
    account = {
        name: 'Buster',
    };
}
<!-- childCmp.html -->
<template>
    <template if:true={record}>
        <p>{record.name}</p>
        <button onclick={updateAccount}>Update record.name</button>
    </template>
</template>
// childCmp.js
import { LightningElement, api } from 'lwc';
export default class ChildCmp extends LightningElement {
    @api record;

    updateAccount() {
        this.record.name = 'Test'; // this line throw error
    }
}

Solution #1: Using Getters and Setters

Close

One of the easy way to accomplish it is cloning value in the getter method. It's easy by using the spread operator.
<!-- parentCmp.html -->
<template>
    <c-child-cmp account={account}></c-child-cmp>
</template>
// parentCmp.js
import { LightningElement } from 'lwc';
export default class ParentCmp extends LightningElement {
    account = {
        name: 'Buster',
    };
}
<!-- childCmp.html -->
<template>
    <template if:true={record}>
        <p>{record.name}</p>
        <button onclick={updateAccount}>Update record.name</button>
    </template>
</template>
// childCmp.js
import { LightningElement, track, api } from 'lwc';
export default class ChildCmp extends LightningElement {
    @track record;

    @api
    get account() {
        return this.record;
    }

    set account(value) {
        this.record = {...value};
    }

    updateAccount() {
        this.record.name = 'Test';
    }
}

Solution #2: Using custom set method

Close

You can create the @api method on the child component. In this way, you do not need to clone a value.

<!-- parentCmp.html -->
<template>
    <c-child-cmp></c-child-cmp>
    <button onclick={setRecordOnChildCmp}>Set record on child component</button>
</template>
// parentCmp.js
import { LightningElement } from 'lwc';
export default class ParentCmp extends LightningElement {
    account = {
        name: 'Buster',
    };

    setRecordOnChildCmp() {
        this.template.querySelector('c-child-cmp').setRecord(this.account);
    }
}
<!-- childCmp.html -->
<template>
    <template if:true={record}>
        <p>{record.name}</p>
        <button onclick={updateAccount}>Update record.name</button>
    </template>
</template>
// childCmp.js
import { LightningElement, track, api } from 'lwc';
export default class ChildCmp extends LightningElement {
    @track record;

    @api
    setRecord(value) {
        return this.record = value;
    }

    updateAccount() {
        this.record.name = 'Test';
    }
}

2. One way data binding in inputs

I'm used to aura inputs and it was a surprise for me that the lwc controller variable was not changed. But it works as designed, the data binding between components for property values is one-way. You need to put onchange event on each input. Lucky it can be handled by following simple approach:

<!-- exampleCmp.html -->
<template>
    <p>Hello, {contact.firstName} {contact.lastName}!</p>

    <lightning-input label="First Name" value={contact.firstName}
                     name="firstName" onchange={handleOnChange}>
    </lightning-input>

    <lightning-input label="Last Name" value={contact.lastName}
                     name="lastName" onchange={handleOnChange}>
    </lightning-input>
</template>
// exampleCmp.js
import { LightningElement, track } from 'lwc';
export default class ParentCmp extends LightningElement {
    @track contact = {
        firstName: 'Rick',
        lastName: 'Sanchez',
    }

    handleOnChange(event) {
        const { name, value } = event.target;
        this.contact[name] = value;
    }
}

3. Shadow DOM

In LWC the CSS is completely isolated, it's not the same as Aura. CSS styles defined in components don't leak into a child, parent, or sibling CSS. But Salesforce Lightning do not use a real Shadow DOM, they polyfilled the Shadow DOM behavior since not all browser support it.

Let's look at the next example, a p style defined in the parentCmp.css style sheet does not style the p element in the c-child-cmp component.

<!-- parentCmp.html -->
<template>
    <p>It is the parent component!</p>
    <c-child-cmp></c-child-cmp>
</template>
/* parentCmp.css */
p {
    color: green;
}
<!-- childCmp.html -->
<template>
    <p>It is the child component!</p>
</template>

It is the parent component!

It is the child component!

4. Utils to work with custom labels

We do not need to worry about importing custom labels in Aura but in LWC you need to import every single label. It's annoying to import the same labels in each component. And there's a way out, you can create a separate LWC component to hold custom labels. Do not put all labels in one component, group them by modules where it used. It's required to create a @track property to allow using labels on the html markup.

// labels.js
import greeting from '@salesforce/label/c.greeting';
import salesforceLogoDescription from '@salesforce/label/c.salesforceLogoDescription';

export const label = {
    greeting,
    salesforceLogoDescription
};
The example below demonstrates using labels util:
<!-- exampleCmp.html -->
<template>
    <p>{label.greeting}</p>
    <!-- <p>Hello World!</p>  -->
</template>
// exampleCmp.js
import { LightningElement, track } from 'lwc';
import { label } from 'c/labels';

export default class ParentCmp extends LightningElement {
    @track label = label;

    connectedCallback() {
        console.log(label.greeting); // Hello World!
    }
}

5. Share JavaScript Code

I've got great news for you. LWC allows us to easily share javascript code. You just need to create a module and export the variables or functions that you want to share. Please see the example below, it's utils component that contains show toast, navigate to record and error parse methods. You can use it as starting point :)

// utils.js
import { ShowToastEvent }   from 'lightning/platformShowToastEvent';
import { NavigationMixin }  from 'lightning/navigation';

//---------------------------------------------------------------------------------
// public methods
const showSuccessToast = (component, message, title = 'Success') => {
    showToast(component, 'success', title, message);
}

const showFailToast = (component, message, title = 'Fail') => {
    showToast(component, 'error', title, message);
}

const navigateToRecordPage = (component, recordId) => {
    const actionName = 'view';
    component[NavigationMixin.Navigate]({
        type: 'standard__recordPage',
        attributes: { recordId, actionName },
    });
}

const parseErrors = (errors) => {
    const messages = [];
    
    if (errors) {
        errors = [].concat(errors);
        errors.forEach((error) => {
            if (error.pageErrors) {
                error.pageErrors.forEach((pageError) =>
                    messages.push(pageError.message));
            } 

            if (error.fieldErrors) {
                for (let fieldName in error.fieldErrors) {
                    error.fieldErrors[fieldName].forEach((fieldError) =>
                        messages.push(`${fieldName}: ${fieldError.message}`));
                }
            }

            if (error.message) {
                messages.push(error.message);
            }
        });
    }

    return messages.length ? messages.join('\n') : "Unknown Error";
}

export {
    showSuccessToast,
    showFailToast,
    navigateToRecordPage,
    parseErrors
}

//---------------------------------------------------------------------------------
// private methods
const showToast = (component, variant, title, message, mode = 'dismissable') => {
    const e = new ShowToastEvent({ title, message, variant, mode });
    component.dispatchEvent(e);
}
The example below demonstrates using these utils:
// exampleCmp.js
import { LightningElement } from 'lwc';
import { showSuccessToast } from 'c/utils';

export default class ParentCmp extends LightningElement {
    connectedCallback() {
        showSuccessToast(this, 'The component is inserted into a document');
    }
}



Please feel free to contact me if you have any questions.


Useful links:

Comments powered by CComment