Skip to content

A Web Component abstraction to declaratively render elements that depend on asynchronous processes

Notifications You must be signed in to change notification settings

jorenbroekema/suspense-element

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

39 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

suspense-element

A Web Component abstraction to declaratively render elements that depend on asynchronous processes. This component has 0 dependencies and is made with native browser technologies only.

Inspired by React Suspense and Pending Task Protocol proposal.

Loading... Error :(

See the demo or run it locally with npm start after installing.

Features

  • Allows to easily render fallback content when the main-element is still loading (either itself or internal async processes).
  • Allows to easily render error content in case any of the internal async processes reject.

Usage

Installation

Installing with NPM

npm i suspense-element

Import the class and define it on the registry yourself, or import the custom elements definition to have it done for you.

import { SuspenseElement } from 'suspense-element';

// Or

import 'suspense-element/define';

Render the suspense element with a fallback slot, optionally an error slot.

The main element that is expected to fire a PendingTaskEvent, see example code further down.

<suspense-element>
  <span slot="fallback">Loading...</span>
  <span slot="error">Error :(</span>
  <main-element></main-element>
</suspense-element>

Demo resolve

If you're viewing the docs site, below is a demo of the suspense-element where the main element pending task is resolved, in action.

Loading... Error :(

Demo reject

If you're viewing the docs site, below is a demo of the suspense-element where the main element pending task is rejected, in action.

Loading... Error :(

Example main-element

import { PendingTaskEvent } from 'suspense-element';

class MainElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });

    /**
     * Set property to promise initially
     * @type {Promise<void>}
     */
    this.list = new Promise((resolve /* , reject */) =>
      setTimeout(() => {
        this.listData = ['foo', 'bar', 'qux'];
        this.render();
        resolve();
        // reject(); // <-- if you want to see the error fallback, make this suspense reject
      }, 1000),
    );

    // Alternatively, combine multiple promises in a wrapping Promise.all()

    this.dispatchEvent(new PendingTaskEvent(this.list));
  }

  render() {
    if (this.shadowRoot) {
      this.shadowRoot.innerHTML = `
      <h1>Hello, World!</h1>
      ${this.listData
        .map(
          (item) => `
          <li>${item}</li>
        `,
        )
        .reduce((acc, item) => acc.concat(item), '')}
    `;
    }
  }
}

Demo subsequent pending tasks

If you're viewing the docs site, below is a demo of the suspense-element where the main element fires pending tasks in 3 second intervals, each resolving in 1 second. It switches between resolving and rejecting.

Loading... Error :(

ResetErrorEvent

When sending multiple pending tasks, either in a single event, stacking multiple, or in subsequent (unstacked) pending tasks, suspense-element has to decide what to do when any of these tasks throw. It will display the error slot if it encounters any error. It will keep doing so even if all pending tasks have completed (some threw), and you send a new one completely separately. The reason for this behavior is that when your main element depends on asynchronous tasks, and one of them throws at any point, new pending tasks do not mean a recovery from old errors even if the new task resolves, so it makes more sense to maintain the error state.

If you need to recover from this you can do so manually, by sending a ResetErrorEvent to the suspense-element. This will reset the internal error state and re-evaluate if there are any pending tasks, if so, render the fallback slot.

import { ResetErrorEvent } from 'suspense-element';

class MainElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.dispatchEvent(new PendingTaskEvent(
      new Promise((resolve, reject) => setTimeout(reject, 100)),
    ));

    setTimeout(() => {
      this.dispatchEvent(new ResetErrorEvent());
      // error slot is still displayed, but sending a new pending task event 
      // will set the state to 'pending' and display fallback slot.
    }, 110);
  }
}

Rationale

In my opinion, it is often easier and better to handle conditional rendering based on asynchronous processes in the main-element itself. This suspense-element is just a helper, an alternative, to do this declaratively in HTML instead in case people prefer that... Similar to React.Suspense, I would not recommend it for common usage, honestly, but perhaps I am not aware of some of its niche use cases where it works well.

There is one hard coupling between the suspense-element and the main-element. main-element must dispatch a PendingTaskEvent with a complete property that contains a Promise (or multiple promises wrapped in a Promise.all()). This is necessary for the suspense-element to know for which internal asynchronous processes it should suspend displaying the main-element and display the fallback instead. It also uses this to watch for any of these internal processes throwing, and render the error content in that case.

About

A Web Component abstraction to declaratively render elements that depend on asynchronous processes

Resources

Stars

Watchers

Forks

Packages

No packages published