7

Is there a way in LWC to declare an attribute value change handler like there was in Aura? My use case is that I have a pager component which has 2 @api properties. When either of the @api values are changed, i'd like to reset the state of the component.

import { LightningElement, api, track } from 'lwc';

export default class Pager extends LightningElement {
    @api pagerData;  // rows of data being paged
    @api pageSize;   // the size of the page

    @track currentPage;  // 1s based page number (i.e. 1 is first 'index')

    ... 
    // (other properties that get number of pages, etc.) 
    ...

}

So when, for example, pageSize is changed by the parent component, then I'd like to reset the currentPage to 1.

To be a little more explicit, the parent component would have something like this in their markup / js

// testpage.html ----------------------------------------
<template>
    <c-pager page-data={myQueryResult} page-size=2></c-pager>
</template>

// testpage.js ------------------------------------------
import { LightningElement, api, track } from 'lwc';

    ...

    onSomeEvent(event){
       let pagerCmp = this.template.querySelector('c-pager');
       pagerCmp.pageSize = 3;
       //------------------------
       // pagerCmp should handle changes to its own internal state, 
       //  in this case it should reset its currentPage property to 1
       //------------------------
    }

    ...

}

Basically i'd like to write some code in the pager component to call a setup() function when the value of pageSize is changed, since the change to the @api parameters will warrant a reset in the internal state of the component. I'm not sure how to do this in LWC. Any ideas?

Thanks in advance.

  • No, it sounds like the situation is a little different. In the possible duplicate, it seems as though the child component is trying to update state in the parent component, to which the answer is to fire a CustomEvent. In this question, it's the parent that is updating the child component's attributes, and the component needs to update its internal state related to the new attributes. But I appreciate the link. It was a good read. – Vincent Ip Mar 12 at 19:12
  • could you update your post, ill retract my close vote for the duplicate =) thanks! – glls Mar 12 at 19:15
  • Sorry, long time lurker, first time poster. I updated my question a tiny bit. hopefully it's a tad more clear on the use case. Is this all that you were looking for me to update? Again, thanks for your time. – Vincent Ip Mar 12 at 19:27
  • can you add markup of how pagesize you are changing? Ideally you have to add an on change on the markup. WHich is smilar to change of textfield change being notfied to js controller – Pranay Jaiswal Mar 12 at 19:28
  • Hi Pranay, i updated my post to make more explicit where / who is making the update to the pageSize parameter. Hope this helps. – Vincent Ip Mar 12 at 19:45
6

You can use getters and setters like the following to detect changes when a parent component passes a new @api value.

Per the docs:

To execute logic each time a public property is set, write a custom setter. 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.

Parent.html

<template>
  <lightning-input onchange={changePageSize}></lightning-input>
  <c-child page-size={pageSize}></c-test-child>
</template>

Parent.js

@track pageSize;
changePageSize(evt) {
  this.pageSize = evt.target.value;
}

Child.html

<template>
  current page size: {_pageSize}
</template>

Child.js

@api
get pagerData() {
  return this._pagerData;
}
set pagerData(value) {
  this.setAttribute('pagerData', value);
  this._pagerData = value;
  this.setup();
}

@api
get pageSize() {
  return this._pageSize;
}
set pageSize(value) {
  this.setAttribute('pageSize', value);
  this._pageSize = value;
  this.setup();
}

// private
@track _pagerData;
@track _pageSize;

connectedCallback() {
  this.setup();
}

setup() {
  console.log('hello');
}

Additional getter/setter examples here.

Furthur reading on the setAttribute function and what it actually does to a template.

As a FYI, renderedCallback() fires twice, and I'm not seeing any kind of private _isRendered check in your code so it's probably worth moving it to connectedCallback() unless you have a strong reason to put it there.

  • ok, THIS sounds right to me! Let me implement and i'll set this as the answer when I confirm. Thanks for the great info. – Vincent Ip Mar 13 at 15:10
  • Again, thanks for the info. Just to be complete and respond to your comments, in the previous code that I used, I needed to re-evaluate on any possible changes to the @api variables throughout the components lifecycle, not just at initialization. From my understanding, connectedCallback() runs basically on the components initial DOM insertion. Luckily changes to @api properties would also cause a render event, so that's the reason for putting code in there. Anyway, this is all a moot point now. Thanks for your input! – Vincent Ip Mar 13 at 19:04
  • One last comment, in my testing, the getters / setters that you have in the answer won't work as-is. the getter needs to return a differently named property than what the getter is named (or else you'll get a 'Maximum call stack size exceeded' because of the recursion), and the setters are missing the set statements for those properties. – Vincent Ip Mar 13 at 19:33
  • @VincentIp I just did a quick test, I think it depends on how you track and feed the @api vars. For example, I have a lightning-input that is setting a @tracked variable to feed down to a child component and each of the setters are only firing once. Yes, you can use private variables to separate your concerns if there is some kind of recursion - but it seems to me this is more due to how your parent changes values on the child (rather than the child setters itself) – tsalb Mar 14 at 1:05
  • @VincentIp ah I spoke too soon. If you show the variable in your view in any way, like {pageSize} then you do get a call stack issue. I will update my answer. – tsalb Mar 14 at 1:30
0

I ended up with a solution that works, but not sure if it's ideal. Here's the code sample of what I ended up with.

import { LightningElement, api, track } from 'lwc';

export default class Pager extends LightningElement {
    @api pagerData;
    @api pageSize;

    prevPagerData;  // added to detect change in @api pagerData
    prevPageSize;   // added to detect change in @api pageSize

    @track currentPage;

    ...

    renderedCallback() {
        if (this.prevPageSize !== this.pageSize) {
            this.prevPageSize = this.pageSize;

            this.currentPage = 1;
            this.setupPageSize();
        }
        if (this.prevPagerData !== this.pagerData) {
            this.prevPagerData = this.pagerData;

            this.currentPage = 1;

            ...

        }
    }
    setupPageSize() { ... }

    ...

}

I had to add private properties that would track the last seen values of the @api variables that I wanted to add value change handlers to, and then in the renderedCallback(), I compare the current @api values with these private variables, and if different do stuff.

It works, but I wonder if there are better ways.

  • when pagerCmp.pageSize = 3; changes, in child all methods who use pageSize are being called again. – Pranay Jaiswal Mar 12 at 21:26
  • I'm not sure I understand your comment. You're saying that with this implementation that if the parent component changes the pageSize property, that within the component, all methods that reference the property will run? I don't think that's happening. – Vincent Ip Mar 13 at 18:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.