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 { // @ts-ignore const edges: Record = {}; return new Proxy(value, { set: (target, property, value) => { setValue((v) => ({ ...v, [property]: value })); return true; }, // @ts-expect-error get: (target, property: keyof T) => { if (property in edges) { // @ts-ignore return edges[property]; } const keyValue = target[property]; const set = (next: SetStateAction) => { const v = typeof next === 'function' ? next(keyValue) : next; setValue((value) => ({ ...value, [property]: v })); }; const edge = createEdge(keyValue, set); edges[property] = edge; return edge; }, deleteProperty: (target, property) => { setValue((v) => { const newValue = { ...v }; // @ts-ignore delete newValue[property]; return newValue; }); return true; }, }); } const inPlaceArrayOperations = [ 'fill', 'reverse', 'push', 'pop', 'shift', 'unshift', ]; function createEdgeForArray( value: PlainJSArray, setValue: Dispatch>> ) { const edges = {} as Record; const set = (property: number, next: SetStateAction) => { const current = value[property]; const nextValue = typeof next === 'function' ? next(current) : next; setValue((value) => [ ...value.slice(0, property), nextValue, ...value.slice(property + 1), ]); }; return new Proxy(value, { set: (target, property, value) => { if (typeof property === 'number') { set(property, value); } return true; }, // @ts-expect-error get: (target, property: keyof PlainJSArray[]) => { if ( typeof property === 'number' || (typeof property === 'string' && /\d+/.test(property)) ) { property = +property; if (property in edges) { return edges[property]; } const item = target[property]; const setThis = set.bind(null, property); const edge = createEdge(item, setThis); edges[property] = edge; return edge; } else { // @ts-ignore if (inPlaceArrayOperations.includes(property)) { return function () { const newValue = [...value]; const method = newValue[property]; // @ts-expect-error const result = method.apply(newValue, arguments); setValue(newValue); return result; }; } else if (typeof target[property] === 'function') { return function () { // @ts-ignore return target[property].apply(target, arguments); }; } 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]; }