@@ -49,7 +49,7 @@ type TimeSyncInitOptions = Readonly<{
49
49
/**
50
50
* The function to use when clearing intervals.
51
51
*
52
- * (i.e ., Clearing a previous interval because the TimeSync needs to make a
52
+ * (e.g ., Clearing a previous interval because the TimeSync needs to make a
53
53
* new interval to increase/decrease its update speed.)
54
54
*/
55
55
clearInterval : ClearInterval ;
@@ -65,7 +65,7 @@ const defaultOptions: TimeSyncInitOptions = {
65
65
66
66
type SubscriptionEntry = Readonly < {
67
67
id : string ;
68
- maxRefreshIntervalMs : number ;
68
+ idealRefreshIntervalMs : number ;
69
69
onUpdate : ( newDatetime : Date ) => void ;
70
70
} > ;
71
71
@@ -136,15 +136,15 @@ export class TimeSync implements TimeSyncApi {
136
136
}
137
137
138
138
const prevFastestInterval =
139
- this . #subscriptions[ 0 ] ?. maxRefreshIntervalMs ?? Number . POSITIVE_INFINITY ;
139
+ this . #subscriptions[ 0 ] ?. idealRefreshIntervalMs ?? Number . POSITIVE_INFINITY ;
140
140
if ( this . #subscriptions. length > 1 ) {
141
141
this . #subscriptions. sort (
142
- ( e1 , e2 ) => e1 . maxRefreshIntervalMs - e2 . maxRefreshIntervalMs ,
142
+ ( e1 , e2 ) => e1 . idealRefreshIntervalMs - e2 . idealRefreshIntervalMs ,
143
143
) ;
144
144
}
145
145
146
146
const newFastestInterval =
147
- this . #subscriptions[ 0 ] ?. maxRefreshIntervalMs ?? Number . POSITIVE_INFINITY ;
147
+ this . #subscriptions[ 0 ] ?. idealRefreshIntervalMs ?? Number . POSITIVE_INFINITY ;
148
148
if ( prevFastestInterval === newFastestInterval ) {
149
149
return ;
150
150
}
@@ -187,9 +187,9 @@ export class TimeSync implements TimeSyncApi {
187
187
} ;
188
188
189
189
subscribe = ( entry : SubscriptionEntry ) : ( ( ) => void ) => {
190
- if ( entry . maxRefreshIntervalMs <= 0 ) {
190
+ if ( entry . idealRefreshIntervalMs <= 0 ) {
191
191
throw new Error (
192
- `Refresh interval ${ entry . maxRefreshIntervalMs } must be a positive integer (or Infinity)` ,
192
+ `Refresh interval ${ entry . idealRefreshIntervalMs } must be a positive integer (or Infinity)` ,
193
193
) ;
194
194
}
195
195
@@ -207,7 +207,7 @@ export class TimeSync implements TimeSyncApi {
207
207
}
208
208
209
209
this . #subscriptions[ subIndex ] = entry ;
210
- if ( prev . maxRefreshIntervalMs !== entry . maxRefreshIntervalMs ) {
210
+ if ( prev . idealRefreshIntervalMs !== entry . idealRefreshIntervalMs ) {
211
211
this . #reconcileRefreshIntervals( ) ;
212
212
}
213
213
return unsub ;
@@ -246,7 +246,7 @@ export const TimeSyncProvider: FC<TimeSyncProviderProps> = ({
246
246
// Making the TimeSync instance be initialized via React State, so that it's
247
247
// easy to mock it out for component and story tests. TimeSync itself should
248
248
// be treated like a pseudo-ref value, where its values can only be used in
249
- // very specific, React-approved ways (e.g., not directly in a render path)
249
+ // very specific, React-approved ways
250
250
const [ readonlySync ] = useState (
251
251
( ) => new TimeSync ( options ?? defaultOptions ) ,
252
252
) ;
@@ -259,14 +259,13 @@ export const TimeSyncProvider: FC<TimeSyncProviderProps> = ({
259
259
} ;
260
260
261
261
type UseTimeSyncOptions < T = Date > = Readonly < {
262
- maxRefreshIntervalMs : number ;
262
+ idealRefreshIntervalMs : number ;
263
263
264
264
/**
265
265
* Allows you to transform any date values received from the TimeSync class.
266
- *
267
- * Note that select functions are not memoized and will run on every render
268
- * (similar to the ones in React Query and Redux Toolkit's default selectors).
269
- * Select functions should be kept cheap to recalculate.
266
+ * Select functions work similarly to the selects from React Query and Redux
267
+ * Toolkit – you don't need to memoize them, and they will only run when the
268
+ * underlying TimeSync state has changed.
270
269
*
271
270
* Select functions must not be async. The hook will error out at the type
272
271
* level if you provide one by mistake.
@@ -292,35 +291,44 @@ type ReactSubscriptionCallback = (notifyReact: () => void) => () => void;
292
291
* interval.
293
292
*/
294
293
export function useTimeSync < T = Date > ( options : UseTimeSyncOptions < T > ) : T {
295
- const { select, maxRefreshIntervalMs } = options ;
294
+ const { select, idealRefreshIntervalMs } = options ;
296
295
297
296
// Abusing useId a little bit here. It's mainly meant to be used for
298
297
// accessibility, but it also gives us a globally unique ID associated with
299
298
// whichever component instance is consuming this hook
300
299
const hookId = useId ( ) ;
301
300
const timeSync = useTimeSyncContext ( ) ;
302
301
303
- // We need to define this callback using useCallback instead of useEffectEvent
304
- // because we want the memoization to be invaliated when the refresh interval
305
- // changes. (When the subscription callback changes by reference, that causes
306
- // useSyncExternalStore to redo the subscription with the new callback). All
307
- // other values need to be included in the dependency array for correctness,
308
- // but their memory references should always be stable
302
+ // We need to define this callback using useCallback instead of
303
+ // useEffectEvent because we want the memoization to be invaliated when the
304
+ // refresh interval changes. (When the subscription callback changes by
305
+ // reference, that causes useSyncExternalStore to redo the subscription with
306
+ // the new callback). All other values need to be included in the dependency
307
+ // array for correctness, but their memory references should always be
308
+ // stable
309
309
const subscribe = useCallback < ReactSubscriptionCallback > (
310
310
( notifyReact ) => {
311
311
return timeSync . subscribe ( {
312
- maxRefreshIntervalMs ,
312
+ idealRefreshIntervalMs ,
313
313
onUpdate : notifyReact ,
314
314
id : hookId ,
315
315
} ) ;
316
316
} ,
317
- [ timeSync , hookId , maxRefreshIntervalMs ] ,
317
+ [ timeSync , hookId , idealRefreshIntervalMs ] ,
318
318
) ;
319
319
320
- const currentTime = useSyncExternalStore ( subscribe , ( ) =>
321
- timeSync . getLatestDatetimeSnapshot ( ) ,
322
- ) ;
320
+ // Unlike subscribe, getNewState doesn't need to be memoized, because React
321
+ // doesn't actually care about its memory reference. Whenever React gets
322
+ // notified, it just calls the latest state function it received (whatever
323
+ // it happens to be). React will only re-render if the value returned by the
324
+ // function is different from what it got back last time (using === as the
325
+ // comparison). This function ONLY gets called when the subscription is
326
+ // first created, or when React gets notified via a subscription update.
327
+ const getNewState = ( ) : T => {
328
+ const latestDate = timeSync . getLatestDatetimeSnapshot ( ) as T & Date ;
329
+ const selected = ( select ?.( latestDate ) ?? latestDate ) as T ;
330
+ return selected ;
331
+ } ;
323
332
324
- const recast = currentTime as T & Date ;
325
- return select ?.( recast ) ?? recast ;
333
+ return useSyncExternalStore < T > ( subscribe , getNewState ) ;
326
334
}
0 commit comments