LWC Getters and Setters – The Last Frontier
Logic is the beginning of wisdom, not the end.
~ Spock.
Introduction
You want to learn how and when to use getters and setters in LWC?
You’re not sure if you’re using it in correct way?
Or you just like to read posts on BTC?
Well – you’re in the right place!
But why exactly you want to write about it?
I’m gonna be honest with you guys – one of the reasons why I’m writing this post is some other developers code which I’ve recently reviewed. It was, more-or-less, something like this:
get showWarningMessage() {
if (!this.items.length) {
const event = new ShowToastEvent({ title: 'Error', message: 'Error', variant: 'error' }));
this.dispatchEvent(event);
return false;
}
return true;
}
<template if:true={showWarningMessage}>
<span>Error occured.</span>
</template>
So as you can see – this developer used the getter in order to dispatch toast event.
Would you do something like that? Hope not – in this example the view layer is controlling whether the toast will fire or not, because the view layer was calling that getter in that case – crazy, right?
Let’s get this right, once and for all.
Getters and setters are for getting or setting data – not for invoking some funky functions.
Documentation
Where to start… Ok, let’s start with SF Documentation! Here’s the link.
Did you read it carefully? There are two rules which are really important and you should remember them:
If you write a setter for a public property, you must also write a getter. Annotate either the getter or the setter with @api, but not both.
It’s a best practice to annotate the getter.
Long story short – if you want to have a public setter, you need to add a getter to – and getter is the one decorated with the @api
:
_items = [];
@api
get items() {
return this._items;
}
set items(items) {
this._items = items;
}
Good practices and use cases
Basics
All right, so far we’ve covered two things:
- what not to do in getters
- when and how to decorate with @api
Let’s cover some use cases – I think an example would come handy in here.
Imagine component called Teams, which will have one property called members decorated with the @api, so we could pass the members from the parent component. The teams component will create two list – one of blue team and second of red team.
I know it’s a simple example, but we’ve got to start from nothing, right?
Here’s the snippet for parent, let’s call it TeamsContainer:
import { LightningElement } from 'lwc';
export default class TeamsContainer extends LightningElement {
members = [
{
id: 0,
name: 'John-117',
team: 'blue'
},
{
id: 1,
name: 'Frederic-104',
team: 'blue'
},
{
id: 2,
name: 'Jerome-092',
team: 'red'
},
{
id: 3,
name: 'Douglas-042',
team: 'red'
}
];
}
<template>
<c-teams members={members}></c-teams>
</template>
And snippet for template of Teams component:
<template>
<lightning-layout>
<lightning-layout-item padding="around-small" size="6">
<lightning-card title="Blue team">
<ul class="slds-list_dotted">
<li for:each={blueTeam} for:item="member" key={member.id}>
<span>{member.name}</span>
</li>
</ul>
</lightning-card>
</lightning-layout-item>
<lightning-layout-item padding="around-small" size="6">
<lightning-card title="Red team">
<ul class="slds-list_dotted">
<li for:each={redTeam} for:item="member" key={member.id}>
<span>{member.name}</span>
</li>
</ul>
</lightning-card>
</lightning-layout-item>
</lightning-layout>
</template>
To save your imagination, here’s the screenshot of what we want to achieve here:
So as you can see – it’s very simple component with tho columns in order to separate the Blue team from Red team members.
Here are some options – we’ll discuss solutions later.
Option 1:
import { LightningElement, api } from 'lwc';
export default class Teams extends LightningElement {
@api members;
get blueTeam() {
return this.members?.filter(member => member.team === 'blue');
}
get redTeam() {
return this.members?.filter(member => member.team === 'red');
}
}
Option 2:
import { LightningElement, api } from 'lwc';
export default class Teams extends LightningElement {
_members;
@api
get members() {
return this._members;
}
set members(members) {
this._members = members;
}
get blueTeam() {
return this.members?.filter(member => member.team === 'blue');
}
get redTeam() {
return this.members?.filter(member => member.team === 'red');
}
}
Option 3:
import { LightningElement, api } from 'lwc';
export default class Teams extends LightningElement {
_members;
blueTeam;
redTeam;
@api
get members() {
return this._members;
}
set members(members) {
this._members = members;
this.blueTeam = members.filter(member => member.team === 'blue');
this.redTeam = members.filter(member => member.team === 'red');
}
}
What do you think? Are all of those solutions correct? Well… depends on a case!
The most elegant is Option 1 – but if it’s most efficient? You’ll see later in one example it doesn’t have to be.
Option 2? If that’s all you’re planning to do with this component – it’s overengineered, Option 1 would be better. But remember this example – we’ll talk about it later.
Ok, so how about Option 3? Again – depends on a case. If that would be end of the logic in this component, the Option 1 would be the best choice – If not and there’s gonna be more and more logic, rendering, recalculations – Option 3 is not so bad after all.
If you want to make some permament data transformations, you can do it in setter:
import { LightningElement, api } from 'lwc';
export default class Teams extends LightningElement {
_members;
@api
get members() {
return this._members;
}
set members(members) {
this._members = members.map(member => ({ ...member, name: member.name.toUpperCase() }));
}
get blueTeam() {
return this.members?.filter(member => member.team === 'blue');
}
get redTeam() {
return this.members?.filter(member => member.team === 'red');
}
}
But if you want to keep the original members data in Teams untact, and you just want to change the way how data presents itself on a template, you can go with this option:
import { LightningElement, api } from 'lwc';
export default class Teams extends LightningElement {
@api members;
get blueTeam() {
return this.members
?.filter(member => member.team === 'blue')
.map(member => ({ ...member, name: member.name.toUpperCase() }));
}
get redTeam() {
return this.members
?.filter(member => member.team === 'red')
.map(member => ({ ...member, name: member.name.toUpperCase() }));
}
}
Tough questions
Looking for informations if and when getters gets updated? Can setter be called before connectedCallback? So many questions, but no answers?
Let’s find out!
We want to change the TeamsContainer component so we can add new members or remove them. Teams component will stay as is. In that way we will check if getters will update values automatically.
import { LightningElement, track } from 'lwc';
export default class TeamsContainer extends LightningElement {
@track members = [
{
id: 0,
name: 'John-117',
team: 'blue'
},
{
id: 1,
name: 'Frederic-104',
team: 'blue'
},
{
id: 2,
name: 'Jerome-092',
team: 'red'
},
{
id: 3,
name: 'Douglas-042',
team: 'red'
}
];
handleAddMembers() {
this.members.push(
{
id: 4,
name: 'Kelly-087',
team: 'blue'
},
{
id: 5,
name: 'Alice-130',
team: 'red'
}
);
}
handleRemoveMembers() {
this.members = [];
}
}
<template>
<c-teams members={members}></c-teams>
<div class="slds-var-m-top_medium slds-var-p-around_small">
<lightning-button label="Add members" onclick={handleAddMembers}></lightning-button>
<lightning-button label="Remove members" class="slds-var-m-left_small" onclick={handleRemoveMembers}></lightning-button>
</div>
</template>
As you can see – I’ve decorated members with @track
. Make sure you understand documentation about reactivity in LWC. In our case – handleRemoveMembers
would work without @track
, but handleAddMembers
won’t.
Ok, let’s imagine I’ve clicked both our buttons, here are the results:
Looks like it does work – without any changes withing Teams component – awesome, right?
I’ve heard question about it once: does component fires all getters when one of the property (not related) changes? Let’s find out.
Best way to find out is to try to simulate it in our code, so I’ve changed previous code a bit by adding new properties – crazy
and lazy
. crazy
is set by parent, lazy
is changed by setTimeout
in connectedCallback
(because I’m lazy and I don’t want to simulate some fetch operations with promise).
Code snippet for Teams component:
export default class Teams extends LightningElement {
@api members;
@api crazy;
lazy = true;
connectedCallback() {
// eslint-disable-next-line @lwc/lwc/no-async-operation
setTimeout(() => {
this.lazy = false;
}, 5000);
}
...
I’m gonna open my developer console, set up some breaking points and refresh the page – will see if getters for blue and red teams will fire more than once!
Ok, let’s do this!
So as you can see in our little experiment – getters for blueTeam and redTeam got fired three (!) times! First one was the initial one, second time – after the setTimeout and third time – manually triggered.
So the question is – knowing that I might have some other actions in my component that might re-trigger getters – should I have getters with filter functions on members
, or should I filter them out in a setter? That’s a good point… As you can see – everything depends on a case, but you need to think logically!
Setters before connectedCallback?
Which came first – the chicken or the egg?
Let’s reuse a tweaked Option 2 code and see what is firing first!
import { LightningElement, api } from 'lwc';
export default class Teams extends LightningElement {
_members;
@api
get members() {
return this._members;
}
set members(members) {
debugger;
this._members = members;
}
connectedCallback() {
debugger;
}
get blueTeam() {
return this.members?.filter(member => member.team === 'blue');
}
get redTeam() {
return this.members?.filter(member => member.team === 'red');
}
}
As you can see – code got stopped on setter first, then on connectedCallback
. That’s because in our case there was a data passed from the parent – remember that!
Let’s stop here for the moment – I’ll show you one more bad example from developers life.
Let’s assume we’ve got two components, Parent
and Child
, Parent
gets the recordName
from APEX based on recordId
and then passes the result to Child
– and then Child
will do some action with it – very simple example:
import { LightningElement } from 'lwc';
import getRecordName from '@salesforce/apex/LazyController.getRecordName';
export default class Parent extends LightningElement {
recordId = '003000000000000001';
recordName;
connectedCallback() {
getRecordName(this.recordId).then(result => (this.recordName = result));
}
}
import { LightningElement, api } from 'lwc';
import getRecordName from '@salesforce/apex/LazyController.getRecordName';
export default class Child extends LightningElement {
@api recordName;
connectedCallback() {
// I have to wait for recordName because it didn't came from parent
// 10 seconds looks ok
setTimeout(() => {
someActionWithThatName(this.recordName);
}, 10000);
}
}
Looks familiar? I’ve seen it a dozen times and always heard one answer: it’s ok, 10 seconds it’s ok, it’s enough.
No, it’s not ok – instead of doing such a things, you have two options – @api
decorated function or getter and setter if you want to store the values:
@api recordName(recordName) {
if (!recordName) {
return;
}
someActionWithThatName(recordName);
}
_recordName;
@api
get recordName() {
return this._recordName;
}
set recordName(value) {
if (this._recordName !== value) {
this._recordName = value;
someActionWithThatName(this._recordName);
}
}
Reassignments of @api decorated property
I assume you’re a pro developer who has the eslint enabled. If you don’t have it – please do it right away.
There’s one rule which you might have heard about – no-api-reassignments.
Remember Option 2? That’s how you can override that rule – but keep in mind – you have to be really aware of what you’re doing.
See snippet below – I’m assigning new values to a "private" property _members
in a newMembersCallback
function.
import { LightningElement, api } from 'lwc';
export default class Teams extends LightningElement {
_members;
@api
get members() {
return this._members;
}
set members(members) {
this._members = members;
}
get blueTeam() {
return this.members?.filter(member => member.team === 'blue');
}
get redTeam() {
return this.members?.filter(member => member.team === 'red');
}
(...)
newMembersCallback(members) {
this._members = members;
}
}
When NOT to use getter and setter
Remember code from Options 3? As you saw – I’ve created getter and setter when I was manipulating the data upon set
– when I was doing something with it. But later on, in next example I’ve just decorated members property with @api – because I wanted to assign the raw data which came from parent to a child property. Property decorated that way will also work like getter and setter in the same time. Remember about KISS rule – overengineering is never a best practice, so when you don’t need getter and setter to be a separated methods – don’t write them.
Let’s summarize when NOT to use getter and setter:
- when you don’t need to manipulate data when assigning it to property – when accessing raw data is good enough
- when you don’t need to override no-api-reassignment rule
- when you don’t have a problem with setting data and accessing it in connectedCallback
- when you just want to show off to your coworkers that you know some fancy way of coding stuff (overengineering)
Not so obvious cases
Asynchronous getters and setters? Technically… possible, but if it’s correct or makes sense? I’m not so sure about it – sounds overcomplicated. Probably you should consider some changes in your code – but ok, there are situations when your setter needs to wait for some other promsie (for example from some different service) to be resolved.
But anyway – let’s try it!
One thing is for sure – you can’t decorate getter or setter with async
– but it doesn’t mean it’s gonna stop us from using await
!
THAT’S NOT GONNA WORK.
async set items(values) {
const result = await this.getSomething();
this._items = [values, result];
}
async set
or set async
is incorrect syntax – simple as that.
But we can always try to return the anonymous function which will return promise, right?
set value(promisedValue) {
return (async () => {
this._value = await promisedValue;
})();
}
Let’s check if that works!
TeamsContainer snippet:
import { LightningElement } from 'lwc';
export default class TeamsContainer extends LightningElement {
members = [
{
id: 0,
name: 'John-117',
team: 'blue'
},
{
id: 1,
name: 'Frederic-104',
team: 'blue'
},
{
id: 2,
name: 'Jerome-092',
team: 'red'
},
{
id: 3,
name: 'Douglas-042',
team: 'red'
}
];
value = () =>
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('It works!');
}, 10000);
});
}
<template>
<c-teams members={members} value={value}></c-teams>
</template>
Teams snippet:
import { LightningElement, api } from 'lwc';
export default class Teams extends LightningElement {
@api members;
get blueTeam() {
return this.members?.filter(member => member.team === 'blue');
}
get redTeam() {
return this.members?.filter(member => member.team === 'red');
}
_value;
@api
get value() {
return this._value;
}
set value(promisedValue) {
return (async () => {
this._value = await promisedValue();
})();
}
}
<template>
<lightning-layout multiple-rows>
<lightning-layout-item padding="around-small" size="6">
<lightning-card title="Blue team">
<ul class="slds-list_dotted">
<li for:each={blueTeam} for:item="member" key={member.id}>
<span>{member.name}</span>
</li>
</ul>
</lightning-card>
</lightning-layout-item>
<lightning-layout-item padding="around-small" size="6">
<lightning-card title="Red team">
<ul class="slds-list_dotted">
<li for:each={redTeam} for:item="member" key={member.id}>
<span>{member.name}</span>
</li>
</ul>
</lightning-card>
</lightning-layout-item>
<lightning-layout-item padding="around-small" size="12">
<lightning-card title="Promised value">
<div class="slds-var-p-around_small">
<span>{value}</span>
</div>
</lightning-card>
</lightning-layout-item>
</lightning-layout>
</template>
Surprise, surprise, after 10 seconds:
The other way how you can utilize async / await for setter:
import { LightningElement, api } from 'lwc';
export default class Awaits extends LightningElement {
_value;
@api
get value() {
return this._value;
}
set value(value) {
this.getAwaitValue(value);
}
async getAwaitValue(values) {
const additionalParameter = await this.getParameter(value);
this._value = await this.getValueWithParameter(value, additionalParameter);
}
}
Ok, what about Promises?
Promises works right away, like here, no tricks involved:
set someValue(values) {
this.loadingPromise.then((result) => this.values = [values, result]);
}
It might come handy when you’re struggling with setter being fired before connectedCallback and you are trying to do something via this setter which requires component to be fully ready.
Conclusion
I hope you’ve enjoyed this long post… and learned something usefull.
Till the nex time!
KP.
Live long and prosper.
~ Spock.
P.S. Pic from Freepik