How to Observe Property Changes with LitElement and TypeScript
Originally published at labs.thisdot.co
Mar 3, 2021 · 5 minutes read · Follow @luixaviles
In a previous post, I explained how to manage LitElement properties using @property
and @internalProperty
decorators through TypeScript. Also, I explained how LitElement manages the declared properties and when it triggers an update cycle, causing the component to re-render its defined template.
A couple of days ago I saw an interesting question regarding these property changes on a developer website:
How to observe @property changes in a LitElement component?
In this article, I will explain how you can observe(and “react” maybe?) these properties considering the lifecycle of a LitElement component.
A Practical Use Case
Let’s suppose you’re building a small application that needs to display a list of Users. You can select any of them and then other details should be displayed, in this case, the Address information.
First Steps
Instead of creating a full project from scratch, we’ll take the @property vs @internalProperty demo as the starting point. So open that link and create a fork to follow up with the next steps.
Project Content
Once you complete the fork, you’ll have a project ready with:
- The Data Model, using TypeScript interfaces and static typing.
- A Data Set, which exports two constants for easy testing:
users
andaddress
. - Two Web Components using LitElement
- The
MainViewer
Component, which is a container component that will be in charge of displaying both the list of users and their details. - The
AddressViewer
Component, which is a child component that receives theuserId
selected in the main list.
- The
Property Changes
Let’s take a closer look at the existing components to understand the main properties that will be considered later.
The main-viewer.ts
file defines the following template:
<div>
<h1>User Address Viewer</h1>
<!-- The User list is rendered here -->
<!-- Child component rendered below -->
<address-viewer .userId="${this.userId}"></address-viewer>
</div>
The code snippet shows the relevance of the userId
property, declared as part of the MainViewer
component: Every time it changes, the render function will be triggered sending the new property value to its child component(<address-viewer userId=${newValue}>
).
On other hand, the address-viewer.ts
file defines the public property userId
and determines the need of an internal property to have the Address
object:
// address-viewer.ts
@customElement('address-viewer')
class AddressViewer extends LitElement {
@property({ type: Number }) userId: number;
@internalProperty() userAddress?: Address;
}
Let’s focus on the userId
property of the AddressViewer
component to understanding how we can observe the changes.
Observing the Property Changes
When a new value is set to a declared property, it triggers an update cycle that results in a re-rendering process. While all that happens, different methods are invoked and we can use some of them to know what properties have been changed.
The Property’s setter method
This is the first place where we can observe a change. Let’s update the @property()
declaration in the address-viewer.ts
file:
// address-viewer.ts
@customElement('address-viewer')
class AddressViewer extends LitElement {
@property({ type: Number }) set userId(userId: number) {
const oldValue = this._userId;
this._userId = userId;
this.requestUpdate('userId', oldValue);
}
private _userId: number;
constructor() {
super();
}
get userId() {
return this._userId;
}
}
As you can see, we just added both set
and get
methods to handle the brand new _userId
property.
Creating a new private property is a common practice when the accessor methods are used. The underscore prefix for _userId
is also a code convention only.
However, since we’re writing our setter method here, we may need to call the requestUpdate
method manually to schedule an update for the component. In other words, LitElement generates a setter method for declared properties and call the requestUpdate
automatically.
The requestUpdate
method
This method will be called when an element should be updated based on a component state or property change. It’s a good place to “catch” the new value before the update()
and render()
functions are invoked.
// address-viewer.ts
requestUpdate(name?: PropertyKey, oldValue?: unknown) {
const newValue = this[name];
// You can process the "newValue" and "oldValue" here
// ....
// Proceed to schedule an update
return super.requestUpdate(name, oldValue);
}
In this case, the method will receive the name of the property that has been changed along with the previous value: oldValue
. Make sure to call super.requestUpdate(name, oldValue)
at the end.
The update
method
Another option to observe the property changes. See the example below:
update(changedProperties: Map<string, unknown>) {
if (changedProperties.has("userId")) {
const oldValue = changedProperties.get("userId") as number;
const newValue = this.userId;
this.loadAddress(newValue);
}
super.update(changedProperties);
}
The update
method has a little different signature compared with the previous options:
The changedProperties parameter is a Map
structure where the keys are the names of the declared properties and the values correspond to the old values(the new values are set already and are available via this.<propertyName>
).
You can use the changedProperties.has("<propName>")
to find out if your property has been changed or not. If your property has been changed, then use changedProperties.get("<propName>")
to get access to the old value. A type assertion must be needed here to match the static type defined by the declared property.
It can be overridden to render and keep updated the DOM. In that case, you must call super.update(changedProperties)
at the end to move forward with the rendering process.
If you set a property inside this method, it won’t trigger another update.
The updated
method
The signature of this method will match with the update
. However, this method is called when the element’s DOM has been updated and rendered. Also, you don’t need to call any other method since you’re at the end of the lifecycle of the component.
updated(changedProperties: Map<string, unknown>) {
console.log(`updated(). changedProps: `, changedProperties);
if (changedProperties.has("userId")) {
// get the old value here
const oldValue = changedProperties.get("userId") as number;
// new value is
const newValue = this.userId;
}
// No need to call any other method here.
}
You can use this method to:
- Identify a property change after an update
- Perform post-updating tasks or any processing after updating.
As a side note, if you set a property inside this method, it will trigger the update process again.
Live Demo
Wanna play around with this code? Just open the Stackblitz editor:
Feel free to reach out on Twitter if you have any questions. Follow me on GitHub to see more about my work.