4

A change of a simple variable mentioned in a getter will trigger a re-render. Why is this not a common pattern to trigger re-rendering in case of object changes?

Let me explain.

LWC components rerender when variables in Javascript getters change that are used in markup. E.g.

<div>{red}</div>

and

_red = 100;

get red() {return this._red;}

someButtonClickHandler(event) {this._red = 200;}

In case of objects, one has to take measures, e.g. annotate with @track:

@track
config = {
    colours: {
        red: 100
    }
};

get red() {return this.config.colours.red;}

someButtonClickHandler(event) {this.config.colours.red = 200;}

An alternative is reassigning the object:

config = {
    colours: {
        red: 100
    }
};

get red() {return this.config.colours.red;}

someButtonClickHandler(event) {
    this.config.colours.red = 200;
    this.config = {...this.config};
}

Now, I haven't come across the following pattern - but does this not work well too?:

rerender = 0;
config = {
    colours: {
        red: 100
    }
};

get red() {
    this.rerender;
    return this.config.colours.red;
}

someButtonClickHandler(event) {
    this.rerender++;
    this.config.colours.red = 200;
}

According to my testing the presence of this.rerender; in the getter makes all the difference. I do not fully understand why. Apparently the framework records during the first rendering cycle that this property is accessed inside the getter and is unable to distinguish between relevant and irrelevant properties.

Even for large objects I don't expect this to be much faster or save relevant memory. But it seems to have its benefits. E.g. this.rerender could be added to many getters and force sweeping re-rendering. It also works for stuff like Maps, where all the above fails:

rerender = 0;
config = new Map().set('red', 100);

get red() {
    this.rerender; 
    return this.config.get('red');
}

someButtonClickHandler(event) {
    this.config.set('red', 200);
    this.rerender++;
}

There is a lack of clarity. A reader of this code feels tempted to remove this.rerender;, because it's not read. One could take precautions and add a comment. Any other disadvantages? Does this always work anyway?

5
  • 1
    Why would rerender as a property cause rerendering? Can you edit to explain please? I haven't confirmed, but I understoof LWC only rerendered the part(s) of the template that reference modified properties. If so, use of spread operator is excessive, causing every part of the template using a property of the object to rerender. Better to use @track. Also, if you need to replace the whole object it would be cleaner to use Object.assign. E.g. this.config = Object.assign({"red": 200}, config);
    – Phil W
    Commented Jul 10, 2022 at 16:31
  • 1
    Right, naming the spread operator as the "other alternative" was rubbish. I corrected this in particular in the title of the question. In the example itself I've kept the spread operator, because code sections compare nicely like this. I've also added a new paragraph and done my best to explain the observed behavior ("According to my testing..."). Note that not the presence of the rerender property triggers the rerendering, but the fact it's being referred to in the getter. Maybe I have overlooked something generally? Commented Jul 10, 2022 at 17:50
  • According to my testing the presence of this.rerender; in the getter makes all the difference. I do not fully understand why. >> Looks like that is how the framework is implemented. According to this, if a field’s value changes, and the field is used in a getter of a property (that’s used in a template), the component rerenders. Since you have this.rerender++ within someButtonClickHandler, it seems to be inline with documentation.
    – arut
    Commented Jul 10, 2022 at 18:42
  • While this alternate approach seems to work, do you see any real benefit in terms of the performance or response times (or EPT where the LWC is placed) when compared to the use of @track on objects?
    – arut
    Commented Jul 10, 2022 at 18:48
  • As stated in the question, I don't see any benefit in terms of performance etc. But for Set, Map it'd be of interest, even if they could be reassigned. Or think of my example above, just with 5 getters that all refer to different Configuration objects, with all 5 Configurations being adjusted in the button handler: a single this.rerender++ could rerender all 5 getters. Commented Jul 10, 2022 at 19:48

1 Answer 1

2

tl;dr; nested components may not render correctly if you use this technique under some circumstances. Take care if you're planning on using this technique.

Using getters this way has some logic bugs that can occur when (a) the property being modified is not in the template's markup, and (b) multiple layers of components use this same technique. I actually ran into this issue some time ago, which led me to abandoning getters being used this way. I wrote a trivial example to demonstrate the problem.

grandparent.html

<template>
  <div onclick={changeToBlue}>
    <c-parent config={config.color}></c-parent>
  </div>
</template>

grandparent.js

import { LightningElement } from 'lwc';

let color = 'red';

export default class Grandparent extends LightningElement {
  rerender = 0;
  get config() {
    return { color: { red: color } };
  }
  changeToBlue() {
    this.rerender++;
    color = 'blue';
  }
}

parent.html

<template>
  <c-child red={config.red}></c-child>
</template>

parent.js

import { LightningElement, api } from 'lwc';

export default class Parent extends LightningElement {
  rendered = 0;
  _config;
  @api set config(value) {
    this.rendered++;
    this._config = value;
  }
  get config() {
    return this._config;
  }
}

child.html

<template>
    I should be: {red}
</template>

child.js

import { LightningElement, api } from "lwc";

export default class Child extends LightningElement {
  @api red;
}

As you can see, the child is never notified of the grandparent's change in data, even when both grandparent and parent are using this reactive "trick."

If I change this to use a class property, instead:

import { LightningElement } from 'lwc';

export default class Grandparent extends LightningElement {
  rerender = 0;
  color = 'red';
  get config() {
    return { color: { red: this.color } };
  }
  changeToBlue() {
    this.rerender++;
    this.color = 'blue';
  }
}

It suddenly starts working.

Note that this is only a problem with components that use other components. As long as all of your rendering is constrained to what we're calling the grandparent component, this issue shouldn't pop up.

4
  • 2
    That's fantastic, thank you so much! The biggest surprise is how someone can come up with such a subtle answer so quickly, up to aligning code with example code. Even if you regard your code example as "trivial", it remains much for me to experiment with and learn from. These days most LWC nest other LWC, but at first glance there still seem to be quite a few situations, where suggested technique appears useful. E.g. I didn't contemplate at all to use rerender as a non-class variable. But I will need some more time to explore - thanks again! Commented Jul 10, 2022 at 20:40
  • Ultimately, in the context of the question, why not stick with the documented and supported ways of working and correctly declare @track object properties?
    – Phil W
    Commented Jul 11, 2022 at 6:09
  • @PhilW This answer is a reason why one should stick with the documented and supported ways of triggering render cycles. The trick, while it does look cool initially, lulls you into a false sense of security before it randomly decides to break your entire project (fun fact: that was my project it broke a few months ago). I would advocate for using the standard methods and not get fancy.
    – sfdcfox
    Commented Jul 11, 2022 at 13:49
  • @sfdcfox absolutely, though there can be many other reasons to stick with supported approaches (such as performance or memory management) that may not be easy to identify without some deep analysis. If the supported approach suffers from a framework bug then it is something you can directly raise in the expectation that it gets fixed. Use of an unsupported approach won't stand much chance of receiving a fix.
    – Phil W
    Commented Jul 11, 2022 at 13:54

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .