lit / lit

Lit is a simple library for building fast, lightweight web components.
https://lit.dev
BSD 3-Clause "New" or "Revised" License
18.76k stars 925 forks source link

[reactive-element] Improve DX for async work in update lifecycle #4385

Closed sorvell closed 12 months ago

sorvell commented 12 months ago

Should this be an RFC?

Which package is this a feature request for?

Lit Core (lit / lit-html / lit-element / reactive-element)

Description

Discord thread.

The performUpdate method can optionally return a promise which is awaited to complete the update. Practically, this means the updateComplete promise is fulfilled and subsequent updates can occur.

To facilitate async update work, the firstUpdated and updated methods should also be able to optionally return a promise that the update completion status awaits.

The performUpdate behavior was added primarily to support scheduling when the update work should start, but it could also be used to perform async work after the Lit update.

Unfortunately, if property changes are needed, then implementing performUpdate doesn't work since this info is not provided. Instead, updated should be used. However, making the update status await work done in updated is inconvenient and requires a construction like this:

private updatedWorkDone: (value: boolean) => void;
performUpdate() {
  if (!this.isUpdatePending) {
    return;
  }
  const done = new Promise((resolve) => this.updatedWorkDone = resolve);
  super.performUpdate();
  return done;
}

async updated(changed) {
  if (changed.has('count')) {
    await new Promise(r => setTimeout(r, 1000));
  }
  this.updatedWorkDone(true);
}

This would be more convenient if updated (and firstUpdated) could just optionally return a promise that's awaited.

Alternatives and Workarounds

A workaround is possible as shown above, but it's inconvenient.

sorvell commented 12 months ago

Unfortunately, changing this behavior in ReactiveElement will add a small amount of new code that might not be worth it for this type of advanced use case. The following construction is a bit more understandable and provides reasonable DX:

private updateCountComplete: Promise<unknown>;
async performUpdate() {
  await this.updateCountComplete;
  super.performUpdate();
}

updated(changed) {
  if (changed.has('count')) {
    this.updateCountComplete = this.updateCount(changed.get('count'))
  }
}

async updateCount(oldCount) {
  await new Promise(r => setTimeout(r, 1000));
  console.log('do async changes for count');
}