perTarget
ts
perTarget<Target extends WeakKey, Args extends unknown[], T, R = void>(
create: (target: Target, ...args: Args) => Service<T, R>,
keyOf?: (...args: Args) => string,
): (target: Target, ...args: Args) => Service<T, R>Gives a service factory the one instance per target and per options caching every built-in service has.
Usage
ts
import { createService, perTarget } from '@studiometa/js-toolkit';
interface FocusProps {
readonly hasFocus: boolean;
}
const useFocus = perTarget((target: Element) =>
createService<FocusProps>({
props: () => ({ hasFocus: target.contains(document.activeElement) }),
start(emit) {
const publish = () => emit({ hasFocus: target.contains(document.activeElement) });
document.addEventListener('focusin', publish);
document.addEventListener('focusout', publish);
return () => {
document.removeEventListener('focusin', publish);
document.removeEventListener('focusout', publish);
};
},
}),
);
// The same service, twice.
useFocus(document.body);
useFocus(document.body);The key is read by meaning, not by spelling
perTarget() sorts object keys at every depth and drops the keys holding undefined. Arrays keep their order.
js
// One service.
useInView(el, { threshold: 0.5, rootMargin: '0px' });
useInView(el, { rootMargin: '0px', threshold: 0.5 });
useInView(el, { threshold: 0.5, rootMargin: '0px', root: undefined });That is deliberate: two callers who mean the same thing should share a source, and an options object written in a different order means the same thing.
When you need a keyOf
Only what the platform owns:
useInView()gives an object root a stable weak identity, because an element cannot be serialized into a key.useMutation()keeps a canonical init, because the DOM's own rules about which fields imply which are not a plain sort.
Everything else is covered by the default.
The target is a WeakKey
The cache is a WeakMap, so a target that is collected takes its entry with it. Nothing has to be cleaned up, and nothing groups observers across targets.