Skip to content

[progressive hydration] self hydrating custom elements #33

@thescientist13

Description

@thescientist13

Overview

As an alternative / complementary approach to #30 , I had been thinking about what it could look like if instead of the framework / runtime being the handler of the hydration, syntax, DSL or to avoid being an opinionated wrapper around Intersection / Mutations observers.

<my-element hydrate="xxx"></my-element>

What if custom elements had the opportunity to self define their own hydration logic? The premise is that a custom element would define a static __hydrate__ method (or whatever) that could be used to encapsulate its own hydration, loading, etc logic, and then the SSR framework mechanism (e.g. community protocol) would just need to extract this logic and inject that it into the runtime.

Example

Given this sample component

const template = document.createElement('template');

template.innerHTML = `
  <style>
    h6 {
      color: red;
      font-size: 25px;
    }

    h6.hydrated {
      animation-duration: 3s;
      animation-name: slidein;
    }

    @keyframes slidein {
      from {
        margin-left: 100%;
        width: 300%;
      }

      to {
        font-size: 25px;
      }
    }
  </style>

  <h6>This is a test</h6>
`;

class TestComponent extends HTMLElement {
  connectedCallback() {
    if (!this.shadowRoot) {
      this.attachShadow({ mode: 'open' });
      this.shadowRoot.appendChild(template.content.cloneNode(true));
    } else {
      const header = this.shadowRoot.querySelector('h6');

      header.style.color = this.getAttribute('color');
      header.classList.add('hydrated');
    }
  }

  // the fun stuff happens here :)
  static __hydrate__() {
    alert('special __hydrate__ function from TestComponent :)');
    window.addEventListener('load', () => {
      const options = {
        root: null,
        rootMargin: '20px',
        threshold: 1.0
      };

      const callback = (entries, observer) => {
        entries.forEach(entry => {
          if(!initialized && entry.isIntersecting) {
            import(new URL('./www/components/test.js', import.meta.url));
          }
        });
      };

      const observer = new IntersectionObserver(callback, options);
      const target = document.querySelector('wcc-test');

      observer.observe(target);
    })
  }
}

export { TestComponent }

customElements.define('wcc-test', TestComponent)

What's nice is that anything could go here since you have full access to the browser, like for IntersectionObserver, MutationObserver, addEventListener, etc. Plus, the runtime overhead is entirely sized by the user, so no extra JS gets shipped except for what the user themselves chooses to include.

So for this scenario, you could just use it as

<wcc-test color="green"></wcc-test>

and in action, it would look like this

wcc-ssr-self-hydration.mov

Observations

So looking to the above recording, we can observe that we get an alert when the hydration logic runs, even though test.js has not loaded. when we scroll down to the intersecting point, test.js loads the custom element, which then initiates the color change and CSS animation.

I think what’s neat is that at a top level, you could still set attributes on static HTML, maybe to preload some data or state, if you’re already running a single pass over the HTML. So could make for a really nice combination of techniques and potentially open the door up to more complex strategies like partial hydration, or resumability, which is even nicer when you think about that you could include a <script type="application/json"> inside a Shadow DOM... 🤔

Feedback

Some good call outs so far to investigate:

  1. Attach / override a __hydrate__ method to a custom element's base class
  2. Understand the cost of Intersection / Mutation Observers (to help inform best practices, usage recommendations)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions