matthewp / haunted

React's Hooks API implemented for web components 👻

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How to best add properties

micahjon opened this issue · comments

We've been using haunted in production for months now and overall it's been working really well.

Lately, as I've been migrating more components, I've run into two paint points regarding custom properties and methods. Take this sample component:

function ButtonGroup({
    buttons = []
}) {
    return html`${buttons.map(buttonTemplate)}`;
}

Storing state without re-render

For this component, I needed to store a list of prior buttons, so I could animate the transition when buttons are added/removed. If I stored priorButtons as a property (or with useState), then I'd trigger a re-render whenever it was set. It also makes it seems like priorButtons is a public API, when it's really supposed to be private, e.g.

function ButtonGroup({
    buttons = [],
    priorButtons = [], // Public property
}) {
    useEffect(() => { 
        this.priorButtons = buttons.slice(); // state.update() will be called again unnecessarily
    }, [buttons]);

    return html`${buttons.map(buttonTemplate)}`;
}

I opted instead to use a WeakMap to store additional "private" state for these components.

const priorButtons = new WeakMap();

function ButtonGroup({
    buttons = []
}) {
    // Keep track of icons from prior render
    useEffect(() => {
        priorButtons.set(this, buttons.slice());
    }, [buttons]);

    return html`${buttons.map(buttonTemplate)}`;
}

This approach keeps priorButtons private and prevents a re-render, but feels kind of clunky.

Adding a class or property right away

Let's say my <button-group> custom element is replacing on older <div class="button-group">...</div>, and should use the same styles and expose the same methods.

The old component creates a <div> and does two things right away:

  • Adds the button-group CSS class
  • Adds the toggleButtons() helper method

These things happen synchronously, but in Haunted, I can't mimic this behavior. I have to wait for several micro-tasks to complete before my <button-group> has either of these, e.g.

function ButtonGroup({
    buttons = []
}) {
    // First time setup
    if (!this.toggleButtons) {
        this.classList.add('button-group');
        Object.defineProperty(this, 'toggleButtons', {
            get() {
                return (obj) => {
                    this.buttons = toggleButtonsHelper(this.buttons, obj);
                };
            },
        });
    }

    return html`${buttons.map(buttonTemplate)}`;
}

Then, I have to update a bunch of code in my app like this:

Before:

const buttonGroup = createButtonGroup();
buttonGroup.toggleButtons(...)

// ...

const thisButtonGroup = container.querySelector('.button-group');

After:

const buttonGroup = createHauntedButtonGroup();
const buttonGroupReady = new Promise((resolve) => {
    Promise.resolve().then(() => Promise.resolve().then(resolve));
});
buttonGroupReady.then(() => buttonGroup.toggleButtons(...));

// ...

// Select it using element name instead of class b/c class is not added synchronously
const thisButtonGroup = container.querySelector('button-group');

Not a huge deal, but just wanted to bring up these use cases in case others shared them.

This is somewhat related to #198. I wouldn't advocate for getting rid of Proxy (I'm sure it was chosen for a good reason), but being able to whitelist some properties (so that non-whitelisted properties don't trigger a re-render) or add some methods synchronously (before first render) would be handy.

Just realized if I set a property that starts with an underscore (e.g. _priorButtons), it doesn't trigger a re-render.

That's great b/c underscored properties are understood to be private. It'd be great if this was in the docs.