react-exo-hooks
Version:
A collection of useful hooks for data structures and logic, designed for efficiency
88 lines (87 loc) • 3.21 kB
JavaScript
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
/**
* Check if a value is a plain object
* @param value The value
* @returns true if a plain object
*/
function isPlainObject(value) {
return (typeof value === 'object' &&
value !== null &&
Object.getPrototypeOf(value) === Object.prototype);
}
/**
* Proxy an object recursively
* @param object The object
* @param setSignal The signal dispatch fn
* @returns The proxied object
*/
function proxyObject(object, setSignal) {
const revocables = [];
for (const key in object) {
const original = object[key];
if (isPlainObject(original)) {
const [subproxy, subrevoke] = proxyObject(original, setSignal);
object[key] = subproxy;
revocables.push(() => {
subrevoke();
object[key] = original;
});
}
}
const proxy = Proxy.revocable(object, {
set(target, prop, newValue) {
if (prop !== 'valueOf' && target[prop] !== newValue)
setSignal((prior) => prior + 1);
const isPlain = isPlainObject(newValue);
if (isPlain) {
const [subproxy, subrevoke] = proxyObject(newValue, setSignal);
revocables.push(() => {
subrevoke();
Reflect.set(target, prop, newValue);
});
return Reflect.set(target, prop, subproxy);
}
else
return Reflect.set(target, prop, newValue);
},
deleteProperty(target, prop) {
if (prop in target)
setSignal((prior) => prior + 1);
return Reflect.deleteProperty(target, prop);
}
});
function revoke() {
proxy.revoke();
for (const subrevoke of revocables)
subrevoke();
}
return [proxy.proxy, revoke];
}
/**
* Create an object state value that auto updates on mutation \
* This hook is recursive into simple object properties. Class instances will remain unaffected
* @note Effects and memos that use this object should also listen for its signal: `+INSTANCE`
* @warn You should revoke the proxy if you're done with render and don't want unforseen complications. Auto-revokes on and `setObject`
* @param initial The initial object
* @returns [object, setObject, forceUpdate, revoke]
*/
export function useObject(initial) {
const revoked = useRef(false);
const [signal, setSignal] = useState(0);
const [object, setObject] = useState(initial);
const [proxy, _revoke] = useMemo(() => proxyObject(object, setSignal), [object]);
const forceUpdate = useCallback(() => {
setSignal((prior) => prior + 1);
}, []);
const revoke = useCallback(() => {
if (!import.meta.hot) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition
_revoke();
revoked.current = true;
}
}, [_revoke]);
useEffect(() => {
return () => revoke();
}, [revoke]);
proxy.valueOf = () => signal;
return [revoked.current ? object : proxy, setObject, forceUpdate, revoke];
}