Skip to content

Commit ceced2d

Browse files
committed
Support HMR for useObject
1 parent 74a53a5 commit ceced2d

File tree

1 file changed

+45
-14
lines changed

1 file changed

+45
-14
lines changed

src/hooks/use-object.ts

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
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'
922
function 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
*/
5173
export 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

Comments
 (0)