-
-
Notifications
You must be signed in to change notification settings - Fork 267
/
Copy pathreact.ts
165 lines (161 loc) · 4.56 KB
/
react.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import {
useCallback,
useDebugValue,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useSyncExternalStore,
} from 'react'
import {
affectedToPathList,
createProxy as createProxyToCompare,
isChanged,
} from 'proxy-compare'
import { snapshot, subscribe } from './vanilla.ts'
import type { Snapshot } from './vanilla.ts'
const useAffectedDebugValue = (
state: object,
affected: WeakMap<object, unknown>,
) => {
const pathList = useRef<(string | number | symbol)[][]>(undefined)
useEffect(() => {
pathList.current = affectedToPathList(state, affected, true)
})
// TODO consider if we can remove this eslint rule exception
// eslint-disable-next-line react-compiler/react-compiler
useDebugValue(pathList.current)
}
const condUseAffectedDebugValue = useAffectedDebugValue
// This is required only for performance.
// Ref: https://github.com/pmndrs/valtio/issues/519
const targetCache = new WeakMap()
type Options = {
sync?: boolean
}
/**
* useSnapshot
*
* Create a local snapshot that catches changes. This hook actually returns a wrapped snapshot in a proxy for
* render optimization instead of a plain object compared to `snapshot()` method.
* Rule of thumb: read from snapshots, mutate the source.
* The component will only re-render when the parts of the state you access have changed, it is render-optimized.
*
* @example A
* function Counter() {
* const snap = useSnapshot(state)
* return (
* <div>
* {snap.count}
* <button onClick={() => ++state.count}>+1</button>
* </div>
* )
* }
*
* [Notes]
* Every object inside your proxy also becomes a proxy (if you don't use "ref"), so you can also use them to create
* the local snapshot as seen on example B.
*
* @example B
* function ProfileName() {
* const snap = useSnapshot(state.profile)
* return (
* <div>
* {snap.name}
* </div>
* )
* }
*
* Beware that you still can replace the child proxy with something else so it will break your snapshot. You can see
* above what happens with the original proxy when you replace the child proxy.
*
* > console.log(state)
* { profile: { name: "valtio" } }
* > childState = state.profile
* > console.log(childState)
* { name: "valtio" }
* > state.profile.name = "react"
* > console.log(childState)
* { name: "react" }
* > state.profile = { name: "new name" }
* > console.log(childState)
* { name: "react" }
* > console.log(state)
* { profile: { name: "new name" } }
*
* `useSnapshot()` depends on the original reference of the child proxy so if you replace it with a new one, the component
* that is subscribed to the old proxy won't receive new updates because it is still subscribed to the old one.
*
* In this case we recommend the example C or D. On both examples you don't need to worry with re-render,
* because it is render-optimized.
*
* @example C
* const snap = useSnapshot(state)
* return (
* <div>
* {snap.profile.name}
* </div>
* )
*
* @example D
* const { profile } = useSnapshot(state)
* return (
* <div>
* {profile.name}
* </div>
* )
*/
export function useSnapshot<T extends object>(
proxyObject: T,
options?: Options,
): Snapshot<T> {
const notifyInSync = options?.sync
// per-proxy & per-hook affected, it's not ideal but memo compatible
const affected = useMemo(
() => proxyObject && new WeakMap<object, unknown>(),
[proxyObject],
)
const lastSnapshot = useRef<Snapshot<T>>(undefined)
let inRender = true
const currSnapshot = useSyncExternalStore(
useCallback(
(callback) => {
const unsub = subscribe(proxyObject, callback, notifyInSync)
callback() // Note: do we really need this?
return unsub
},
[proxyObject, notifyInSync],
),
() => {
const nextSnapshot = snapshot(proxyObject)
try {
if (
!inRender &&
lastSnapshot.current &&
!isChanged(
lastSnapshot.current,
nextSnapshot,
affected,
new WeakMap(),
)
) {
// not changed
return lastSnapshot.current
}
} catch {
// ignore if a promise or something is thrown
}
return nextSnapshot
},
() => snapshot(proxyObject),
)
inRender = false
useLayoutEffect(() => {
lastSnapshot.current = currSnapshot
})
if (import.meta.env?.MODE !== 'production') {
condUseAffectedDebugValue(currSnapshot as object, affected)
}
const proxyCache = useMemo(() => new WeakMap(), []) // per-hook proxyCache
return createProxyToCompare(currSnapshot, affected, proxyCache, targetCache)
}