1- import { useCallback , useEffect , useMemo , useState } from 'react'
1+ import { useCallback , useEffect , useMemo , useRef , useState } from 'react'
2+
3+ /**
4+ * Check if a value is a plain object
5+ * @param value The value
6+ * @returns true if a plain object
7+ */
8+ function isPlainObject ( value : unknown ) : value is object {
9+ return (
10+ typeof value === 'object' &&
11+ value !== null &&
12+ Object . getPrototypeOf ( value ) === Object . prototype
13+ )
14+ }
215
316/**
417 * Proxy an object recursively
@@ -9,26 +22,35 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
922function proxyObject < T extends object > ( object : T , setSignal : React . Dispatch < React . SetStateAction < number > > ) : [ proxy : T , revoke : ( ) => void ] {
1023 const revocables : Array < ( ) => void > = [ ]
1124
25+ for ( const key in object ) {
26+ const original = object [ key as keyof typeof object ]
27+ if ( isPlainObject ( original ) ) {
28+ const [ subproxy , subrevoke ] = proxyObject ( original , setSignal )
29+ object [ key as keyof typeof object ] = subproxy as any
30+ revocables . push ( ( ) => {
31+ subrevoke ( )
32+ object [ key as keyof typeof object ] = original
33+ } )
34+ }
35+ }
36+
1237 const proxy = Proxy . revocable ( object , {
1338 set ( target , prop , newValue ) {
1439 if ( prop !== 'valueOf' && target [ prop as keyof typeof target ] !== newValue ) setSignal ( ( prior ) => prior + 1 )
15- const isPlain = (
16- typeof object === 'object' &&
17- ( object as any ) !== null &&
18- Object . getPrototypeOf ( object ) === Object . prototype
19- )
40+ const isPlain = isPlainObject ( newValue )
2041 if ( isPlain ) {
2142 const [ subproxy , subrevoke ] = proxyObject ( newValue , setSignal )
22- target [ prop as keyof typeof target ] = subproxy
23- revocables . push ( subrevoke )
24- } else target [ prop as keyof typeof target ] = newValue
25- return true
43+ revocables . push ( ( ) => {
44+ subrevoke ( )
45+ Reflect . set ( target , prop , newValue )
46+ } )
47+ return Reflect . set ( target , prop , subproxy )
48+ } else return Reflect . set ( target , prop , newValue )
2649 } ,
2750
2851 deleteProperty ( target , prop ) {
2952 if ( prop in target ) setSignal ( ( prior ) => prior + 1 )
30- delete target [ prop as keyof typeof target ]
31- return true
53+ return Reflect . deleteProperty ( target , prop )
3254 }
3355 } )
3456
@@ -49,19 +71,28 @@ function proxyObject<T extends object> (object: T, setSignal: React.Dispatch<Rea
4971 * @returns [object, setObject, forceUpdate, revoke]
5072 */
5173export function useObject < T extends object > ( initial : T ) : [ object : T , setObject : React . Dispatch < React . SetStateAction < T > > , forceUpdate : ( ) => void , revoke : ( ) => void ] {
74+ const revoked = useRef ( false )
75+
5276 const [ signal , setSignal ] = useState ( 0 )
5377 const [ object , setObject ] = useState ( initial )
5478
55- const [ proxy , revoke ] = useMemo ( ( ) => proxyObject ( object , setSignal ) , [ object ] )
79+ const [ proxy , _revoke ] = useMemo ( ( ) => proxyObject ( object , setSignal ) , [ object ] )
5680
5781 const forceUpdate = useCallback ( ( ) => {
5882 setSignal ( ( prior ) => prior + 1 )
5983 } , [ ] )
6084
85+ const revoke = useCallback ( ( ) => {
86+ if ( ! import . meta. hot ) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition
87+ _revoke ( )
88+ revoked . current = true
89+ }
90+ } , [ _revoke ] )
91+
6192 useEffect ( ( ) => {
6293 return ( ) => revoke ( )
6394 } , [ revoke ] )
6495
6596 proxy . valueOf = ( ) => signal
66- return [ proxy , setObject , forceUpdate , revoke ]
97+ return [ revoked . current ? object : proxy , setObject , forceUpdate , revoke ]
6798}
0 commit comments