Search code examples
reactjspreacthydration

With preact, how to add event listener to a component hydrated from HTML


Consider the following HTML element:

<div id="foo">
    <div>Foo</div>
</div>

How can I write a preact component that, when hydrated from this HTML element, would add event listener to the inner div - typically a onclick listener?

It is explicitely written in preact documentation that the hydrate function _skips most diffing while still attaching event listeners _, but at no point do they explain how to attach the event listeners.

If I write the following component:

const Foo: FunctionalComponent = () => {
    return <><div onClick={() => alert("click");}></div></>;
}

And hydrate it from the HTML element this way:

import {hydrate} from "preact";

document.addEventListener("DOMContentLoaded", () => {
    const fooElement = document.getElementById('foo');

    hydrate(<Foo />, fooElement!); 
}

Then the content of the div (i.e., "foo") is not hydrated into the rendered component. The only way for the content of the div to be hydrated into the rendered component is to write the component this way:

const Foo: FunctionalComponent = () => {
    return <></>;
}

But as you can guess, there is no way to add an event listener with this method.

So, how can we hydrate a component using an HTML element - preserving the HTML element data - and add event listeners?


Solution

  • When you hydrate a component, Preact assumes that the existing HTML matches what the component would render on its first render.

    So, if your server-rendered HTML is:

    <div id="foo">
        <div>Foo</div>
    </div>
    

    Your Preact component should also render the same HTML structure on its first render for hydration to work as expected.

    Here's how you can modify your Foo component to add an onClick listener to the inner div.

    const Foo = () => {
        return (
            <div>
                <div onClick={() => alert('Clicked!')}>Foo</div>
            </div>
        );
    };
    
    

    And for hydrating:

    import { hydrate } from "preact";
    
    document.addEventListener("DOMContentLoaded", () => {
        const fooElement = document.getElementById('foo');
        hydrate(<Foo />, fooElement);
    });
    
    

    This will hydrate the component and attach the onClick event listener to the inner div.

    However, if you really want to preserve the inner HTML ('Foo' in this case) and attach an event listener, you may need to resort to using native DOM methods within a useEffect or componentDidMount lifecycle method.