Skip to content

How to Observe Property Changes with LitElement and TypeScript

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

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 other details should be displayed. In this case, the Address information is displayed.

user-addresses

First Steps

Instead of creating a full project from scratch, we'll take the @property vs @internalProperty demo as the starting point. Open the link and create a fork to follow up with 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 and address.
  • 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 the userId selected in the main list.

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 the 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

This is 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 slightly 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 is 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

Want to 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.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co