diff --git a/src/components/useImmutable.ts b/src/components/useImmutable.ts new file mode 100644 index 0000000..a2cca4f --- /dev/null +++ b/src/components/useImmutable.ts @@ -0,0 +1,97 @@ +import { Dispatch, SetStateAction, useMemo, useState } from 'react'; + +type Primitive = bigint | boolean | null | number | string | symbol | undefined; + +type PlainJS = Primitive | PlainJSObject | PlainJSArray; + +interface PlainJSObject { + [key: string]: PlainJS; +} + +interface PlainJSArray extends Array {} + +function createEdgeForObject( + value: T, + setValue: Dispatch> +): T { + const keys = Object.keys(value); + // @ts-expect-error + const edge: T = {}; + for (let key of keys) { + Object.defineProperty(edge, key, { + enumerable: true, + configurable: false, + get: () => value[key], + set: (v) => void setValue((value) => ({ ...value, [key]: v })), + }); + } + return edge; +} + +function createEdgeForArray( + value: PlainJSArray, + setValue: Dispatch>> +) { + return new Proxy(value, { + set: (target, property, value) => { + if (typeof property === 'number') { + const set = (next: SetStateAction) => { + const v = typeof next === 'function' ? next(value) : next; + const edge = createEdge(v, set); + setValue((v) => [ + ...v.slice(0, property), + edge, + ...v.slice(property + 1), + ]); + }; + + set(value); + } + return true; + }, + // @ts-expect-error + get: (target, property: keyof PlainJSArray[]) => { + if (typeof property === 'number') { + return target[property]; + } else { + if (typeof target[property] === 'function') { + return function () { + const newValue = [...value]; + const method = newValue[property]; + // @ts-expect-error + const result = method.apply(newValue, arguments); + setValue(newValue); + return result; + }; + } else { + return target[property]; + } + } + }, + }); +} + +function createEdge( + value: T, + setValue: Dispatch> +): T { + if (Array.isArray(value)) { + // @ts-expect-error + return createEdgeForArray(value, setValue) as T; + } else if (typeof value === 'object' && value !== null) { + // @ts-expect-error + return createEdgeForObject(value, setValue) as T; + } else { + return value; + } +} + +export default function useImmutable( + initial: T +): [T, Dispatch>] { + const [value, setValue] = useState(initial); + + const edge = useMemo(() => createEdge(value, setValue), [value]); + + return [edge, setValue]; +}