An experimental library for hydrating web components and adding "sprinkles of reactivity" to pre-rendered HTML.
HydroActive is a experimental library for hydrating web components and adding "sprinkles of reactivity" to pre-rendered HTML.
Check out the YouTube video for an in-depth discussion and demo of HydroActive's features. Note that this video discusses a very early version of HydroActive. Versions up through 0.0.5 roughly align with that video, however HydroActive is currently being redesigned and rewritten, so it may not be up to date with the current version of the library.
Most server-side rendering / static site generation solutions bind the client and server together with a shared implementation. Components are "hybrid rendered" by supporting both the client and the server. They are rendered first on the server and usually props are serialized and passed to the client as JSON. The components then rerender on the client and take over interactivity.
While many frameworks tweak this approach in various ways, a few aspects of it have always bothered me:
These are complaints of a generic strawman SSR/SSG solution, each framework is different and has different answers to these particular problems. Instead, I want to think of hydration through a different lens:
Servers are good at rendering HTML and returning it in HTTP responses. That's kind of their thing, we don't need to reinvent that. If you have a Node, Java, Ruby, Haskell, C, or even Fortran server, any of them should be fine. How a server renders HTML is an unrelated implementation detail. HydroActive focuses on taking that pre-rendered HTML and making it interactive on the client.
This means we can think of hydration as a purely deserialization problem. Servers can render web components without any fancy tooling or integrations, all they need to do is render something like:
<my-counter>
<div>The current count is <span>5</span>.</div>
<button>Increment</button>
</my-counter>
Any server can do that, exactly how it does so is unimportant to HydroActive. From here, HydroActive makes it easy to load from this component and make it interactable. It does this by providing an API to define custom elements with useful lifecycle and convenient DOM APIs. One example would be:
import { ElementAccessor, defineComponent } from 'hydroactive';
import { WriteableSignal } from 'hydroactive/signals.js';
// `defineComponent()` creates a web component class based on the given hydrate
// function. The callback is invoked on hydration and provides `host` and `comp`
// parameters with additional functionality to provide interactivity to the
// pre-rendered component. Automatically calls `customElements.define` under the
// hood.
const MyCounter = defineComponent('my-counter', (host) => {
// Interacting with a site using HydroActive is a three-step process:
// 1. Query it - `host.query` queries the DOM for the selector and asserts
// it is found.
const countEl: ElementAccessor<HTMLSpanElement> = host.query('span#count')
// 2. Hydrate it - `.access` asserts that the element does not need to
// be hydrated. Alternatively, we could call `.hydrate()` to trigger
// hydration on a sub-component.
.access();
// 3. Enhance it - `live` creates a `Signal` which wraps the `textContent`
// of `countEl`. The text is interpreted as a `number` and implicitly
// converted. Also whenever `count.set` is called, the `<span>` tag is
// automatically updated.
const count: WriteableSignal<number> = live(countEl, host, Number);
// Prints `5`, hydrated from initial HTML content.
console.log(count());
// HydroActive providers ergonomic wrappers to read elements from the DOM,
// assert they exist, and use them safely. It also types the result based on
// the query, this implicitly has type `ElementAccessor<HTMLButtonElement>`.
const incrementBtn = host.query('button').access();
// HydroActive also provides ergonomic wrapper to bind event listeners.
// This automatically removes and re-adds the listener when `<my-counter>`
// is disconnected from and reconnected to the DOM.
incrementBtn.listen(host, 'click', () => {
// `count.set` automatically updates the underlying DOM with the new
// value.
count.set(count() + 1);
});
});
// For TypeScript, don't forget to type `my-counter` tags as an instance of the
// class.
declare global {
interface HTMLElementTagNameMap {
'my-counter': InstanceType<typeof MyCounter>;
}
}
See the demo for more cool features. You can run the demo locally with
npm start
. The HTML pages contain hard-coded, pre-rendered HTML (remember, how
they get rendered by the server is an implementation detail). The TypeScript files
house the component's implementations and demonstrate different forms of reactivity
and use cases.
Run tests with npm test
. Debug tests with npm run test-debug
and then opening
localhost:8000
.
nvm install
package.json
.npm test
npm install
Make sure no other dependencies were updated.npm run build
(cd dist/ && npm publish)
# Alternatively run `(cd dist/ && npm pack)` to inspect the tarball to be published.
git add . && git commit -m "Release v0.0.1."
git tag releases/0.0.1
git push
git push --tags