Open jaens opened 1 month ago
const EMPTY = Symbol.for("useOnce/EMPTY");
I like the idea of the Symbol
. But IMHO would you mind explaining your use case of useSingleton
? The main idea behind useSingleton
is to create a singleton instance, it doesn't expect the return value of the creation function to be undefined
or null
:
const playerRef = useSingleton(() => new APlayer())
Also, with the current behavior, it is possible to conditionally create the instance:
const [shouldLoad, setShouldLoad] = useState(false);
const instance1Ref = useSingleton(() => {
shouldLoad ? new APlayer() : null
});
const instance2Ref = useSingleton(() => {
props.foo ? new Foo() : null
});
It's not really about the use case (of course there are use cases), it's about the API behaving in surprising and undocumented ways.
Example use cases:
undefined
). Note that useEffect
can not be used due to different semantics - since useSingleton
is a render-time effect, not a mount-time effect.useMemo
which has clear semantics and is more readable.(In my own code, this function is actually called useOnce
for clarity, since it's not really about just singletons)
Why not add a new ref to determine if it has been initialized?
export const useSingleton = <T>(initializor: () => T): SingletonRefObject<T> => {
const initialized = useRef(false);
const r = useRef<T>();
if (!initialized.current) {
r.current = initializor();
initialized.current = true;
}
return r;
};
That works, but is less efficient and doubles the clutter in the React debug panel.
The check in the source code is
!r.current
which will trigger the function every call when the value happens to be 0, "", undefined, null etc.Here's a more universal implementation:
(technically,
EMPTY
can just be{} as const
ifSymbol
support is unavailable)An alternative is to add a
<T extends object>
type constraint, although that seems slightly unintuitive.