Hookless component model for Preact.
(Do not use in production yet.)
I wanted a simpler component and utilities to
- avoid stale closures (with states and props now using a getter function)
- be forgiving to re-render from unintentional child prop changes (with prop-level memoization and auto useEffectEvent()-ing of
on*props) - reduce the previous mentioned case and useMemo()/useCallback()s by having a component setup scope (think class constructor, but render method having the closure access to them)
- make vdom diffing faster if no prop changed (auto memoize jsx / vdom)
- reduce need for hooks (with onProp and onMount event subscription)
This project is an attempt to avoid many performance footguns as I documented here combining it with a simpler component model.
Creates a Preact component from an instance-like model.
import { hookless, createRef, createState } from "@firstack/hookless";
import { html } from "htm/preact";
const Counter = hookless(({ getProps, onMount, onProps, update }) => {
// Component setup logic here ...
// getProps and get<State> functions avoids stale closure issues
const [getCount, setCount] = createState(0);
// Or you could use `let count = 0` and manual update() calls
// No need of useRef anymore
const buttonRef = createRef();
// Event handlers can be defined here. No need of useCallback().
const onClick = () => setCount(getCount() + 1);
// onProps and onMount reduces need for useEffects
onProps((changedProps, oldProps) => {
if (changedProps.includes("resetKey")) {
setCount(0, false);
// getProps() still gives the latest props
}
});
onMount(() => {
buttonRef.current?.focus();
});
return {
render() {
return html`
<button ref=${buttonRef} onClick=${onClick}>
${getProps().label}: ${getCount()}
</button>
`;
},
// You can add methods to the instance model here so that parent components can call them via refs.
// example:
// incrementCounter() {
// setCount(getCount() + 1);
// },
};
}, {
// These are enabled by default
memo: true,
autoEffectEvent: true,
});factory receives:
getProps(): returns the latest propsonProps(handler): runs before render when a prop actually changedonMount(handler): runs on mount; may return cleanupupdate(callback?): forces a rerender; optional callback runs after render flush
The object returned by factory must contain:
render(): returns the component vnode
It may also contain any extra methods or fields you want to keep on the instance model.
options.memo
true: enabled; all props are deep-comparedfalse: disabled; props are compared withObject.is{ only: ["value", "options"] }: deep-compare only those props{ exclude: ["children"] }: deep-compare everything except those props
Notes:
memodefaults totrueonlyandexcludeare mutually exclusive in intent; if both are passed,onlywins
options.autoEffectEvent
true: enabled; function props matchingon[A-Z]are treated as stable event propsfalse: disabled{ include: ["handleClick"] }: additionally treat these prop names as stable event props{ exclude: ["onDebug"] }: remove these prop names from the automaticon[A-Z]matching
Notes:
autoEffectEventdefaults totrue- the default convention is
on[A-Z], soonClickmatches andoneClickdoes not - matched function props do not trigger rerenders by themselves and are proxied so the latest function is still called
Creates local hookless state.
const [getValue, setValue] = createState(initialValue);Arguments:
initialValue: initial state valuedeep: comparison modetrueor omitted: uses deep equalityfalse: usesObject.is
Returned values:
getValue(): stable getter for the latest statesetValue(nextValueOrUpdater, callback?)
setValue() supports:
setValue(nextValue)setValue((prev) => nextValue)setValue(nextValue, callback)setValue(nextValue, false)to update state without scheduling a rerender
Thin re-export of Preact createRef().
Deep equality helper used internally by Hookless.