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.