@@ -68,8 +68,56 @@ class ReactTimeSync implements ReactTimeSyncApi {
68
68
this . #selectionCache = new Map ( ) ;
69
69
}
70
70
71
+ #areValuesDeepEqual( value1 : unknown , value2 : unknown ) : boolean {
72
+ // JavaScript is fun and doesn't have a 100% foolproof comparison
73
+ // operation. Object.is covers the most cases, but you still need to
74
+ // compare 0 values, because even though JS programmers almost never
75
+ // care about +0 vs -0, Object.is does treat them as not being equal
76
+ if ( Object . is ( value1 , value2 ) ) {
77
+ return true ;
78
+ }
79
+ if ( value1 === 0 && value2 === 0 ) {
80
+ return true ;
81
+ }
82
+
83
+ if ( value1 instanceof Date && value2 instanceof Date ) {
84
+ return value1 . getMilliseconds ( ) === value2 . getMilliseconds ( ) ;
85
+ }
86
+
87
+ // Can't reliably compare functions; just have to treat them as always
88
+ // different. Hopefully no one is storing functions in state for this
89
+ // hook, though
90
+ if ( typeof value1 === "function" || typeof value2 === "function" ) {
91
+ return false ;
92
+ }
93
+
94
+ if ( Array . isArray ( value1 ) ) {
95
+ if ( ! Array . isArray ( value2 ) ) {
96
+ return false ;
97
+ }
98
+ if ( value1 . length !== value2 . length ) {
99
+ return false ;
100
+ }
101
+ return value1 . every ( ( el , i ) => this . #areValuesDeepEqual( el , value2 [ i ] ) ) ;
102
+ }
103
+
104
+ const obj1 = value1 as Record < string , unknown > ;
105
+ const obj2 = value1 as Record < string , unknown > ;
106
+ if ( Object . keys ( obj1 ) . length !== Object . keys ( obj2 ) . length ) {
107
+ return false ;
108
+ }
109
+ for ( const key in obj1 ) {
110
+ if ( ! this . #areValuesDeepEqual( obj1 [ key ] , obj2 [ key ] ) ) {
111
+ return false ;
112
+ }
113
+ }
114
+
115
+ return true ;
116
+ }
117
+
71
118
// All functions that are part of the public interface must be defined as
72
- // arrow functions, so that they work properly with React
119
+ // arrow functions, so they can be passed around React without losing their
120
+ // `this` context
73
121
74
122
getTimeSnapshot = ( ) => {
75
123
return this . #timeSync. getTimeSnapshot ( ) ;
@@ -123,14 +171,16 @@ class ReactTimeSync implements ReactTimeSyncApi {
123
171
} ;
124
172
125
173
invalidateSelection = ( id : string , select ?: SelectCallback ) : void => {
126
- const cacheEntry = this . #selectionCache. get ( id ) ;
127
- if ( cacheEntry === undefined ) {
174
+ const prevSelection = this . #selectionCache. get ( id ) ;
175
+ if ( prevSelection === undefined ) {
128
176
return ;
129
177
}
130
178
131
179
const dateSnapshot = this . #timeSync. getTimeSnapshot ( ) ;
132
180
const newSelection = select ?.( dateSnapshot ) ?? dateSnapshot ;
133
- this . #selectionCache. set ( id , { value : newSelection } ) ;
181
+ if ( ! this . #areValuesDeepEqual( newSelection , prevSelection . value ) ) {
182
+ this . #selectionCache. set ( id , { value : newSelection } ) ;
183
+ }
134
184
} ;
135
185
}
136
186
@@ -214,42 +264,8 @@ export function useTimeSync(options: UseTimeSyncOptions): Date {
214
264
return snapshot ;
215
265
}
216
266
217
- function areDepsInvalidated (
218
- oldDeps : readonly unknown [ ] | undefined ,
219
- newDeps : readonly unknown [ ] | undefined ,
220
- ) : boolean {
221
- if ( oldDeps === undefined ) {
222
- if ( newDeps === undefined ) {
223
- return false ;
224
- }
225
- return true ;
226
- }
227
-
228
- const oldRecast = oldDeps as readonly unknown [ ] ;
229
- const newRecast = oldDeps as readonly unknown [ ] ;
230
- if ( oldRecast . length !== newRecast . length ) {
231
- return true ;
232
- }
233
-
234
- for ( const [ index , el ] of oldRecast . entries ( ) ) {
235
- if ( el !== newRecast [ index ] ) {
236
- return true ;
237
- }
238
- }
239
-
240
- return false ;
241
- }
242
-
243
267
type UseTimeSyncSelectOptions < T > = Readonly <
244
268
UseTimeSyncOptions & {
245
- /**
246
- * selectDependencies acts like the dependency array for a useMemo
247
- * callback. Whenever any of the elements in the array change by value,
248
- * that will cause the select callback to re-run synchronously and
249
- * produce a new, up-to-date value for the current render.
250
- */
251
- selectDependencies : readonly unknown [ ] ;
252
-
253
269
/**
254
270
* Allows you to transform any date values received from the TimeSync
255
271
* class. Select functions work similarly to the selects from React
@@ -279,7 +295,7 @@ type UseTimeSyncSelectOptions<T> = Readonly<
279
295
* interval.
280
296
*/
281
297
export function useTimeSyncSelect < T > ( options : UseTimeSyncSelectOptions < T > ) : T {
282
- const { select, selectDependencies , targetRefreshInterval } = options ;
298
+ const { select, targetRefreshInterval } = options ;
283
299
const hookId = useId ( ) ;
284
300
const timeSync = useTimeSyncContext ( ) ;
285
301
@@ -296,7 +312,7 @@ export function useTimeSyncSelect<T>(options: UseTimeSyncSelectOptions<T>): T {
296
312
// be initialized with whatever callback you give it on mount. So for the
297
313
// mounting render alone, it's safe to call a useEffectEvent callback from
298
314
// inside a render.
299
- const selectForOutsideReact = useEffectEvent ( ( date : Date ) : T => {
315
+ const externalSelect = useEffectEvent ( ( date : Date ) : T => {
300
316
const recast = date as Date & T ;
301
317
return select ?.( recast ) ?? recast ;
302
318
} ) ;
@@ -314,31 +330,20 @@ export function useTimeSyncSelect<T>(options: UseTimeSyncSelectOptions<T>): T {
314
330
targetRefreshInterval,
315
331
id : hookId ,
316
332
onUpdate : notifyReact ,
317
- select : selectForOutsideReact ,
333
+ select : externalSelect ,
318
334
} ) ;
319
335
} ,
320
- [ timeSync , hookId , selectForOutsideReact , targetRefreshInterval ] ,
336
+ [ timeSync , hookId , externalSelect , targetRefreshInterval ] ,
321
337
) ;
322
338
323
- const [ prevDeps , setPrevDeps ] = useState ( selectDependencies ) ;
324
- const depsAreInvalidated = areDepsInvalidated ( prevDeps , selectDependencies ) ;
325
-
326
339
const selection = useSyncExternalStore < T > ( subscribe , ( ) => {
327
- if ( depsAreInvalidated ) {
328
- // Need to make sure that we use the un-memoized version of select
329
- // here because we need to call select callback mid-render to
330
- // guarantee no stale data. The memoized version only syncs AFTER
331
- // the current render has finished in full.
332
- timeSync . invalidateSelection ( hookId , select ) ;
333
- }
340
+ // Need to make sure that we use the un-memoized version of select
341
+ // here because we need to call select callback mid-render to
342
+ // guarantee no stale data. The memoized version only syncs AFTER
343
+ // the current render has finished in full.
344
+ timeSync . invalidateSelection ( hookId , select ) ;
334
345
return timeSync . getSelectionSnapshot ( hookId ) ;
335
346
} ) ;
336
347
337
- // Setting state mid-render like this is valid, but we just need to make
338
- // sure that we wait until after the useSyncExternalStore state getter runs
339
- if ( depsAreInvalidated ) {
340
- setPrevDeps ( selectDependencies ) ;
341
- }
342
-
343
348
return selection ;
344
349
}
0 commit comments