@@ -25,12 +25,14 @@ import {
25
25
import { useEffectEvent } from "./hookPolyfills" ;
26
26
27
27
export {
28
- IDEAL_REFRESH_ONE_DAY ,
29
- IDEAL_REFRESH_ONE_HOUR ,
30
- IDEAL_REFRESH_ONE_MINUTE ,
31
- IDEAL_REFRESH_ONE_SECOND ,
28
+ TARGET_REFRESH_ONE_DAY ,
29
+ TARGET_REFRESH_ONE_HOUR ,
30
+ TARGET_REFRESH_ONE_MINUTE ,
31
+ TARGET_REFRESH_ONE_SECOND ,
32
32
} from "utils/TimeSync" ;
33
33
34
+ type ReactSubscriptionCallback = ( notifyReact : ( ) => void ) => ( ) => void ;
35
+
34
36
type SelectCallback = ( newSnapshot : Date ) => unknown ;
35
37
36
38
type ReactSubscriptionEntry = Readonly <
@@ -47,6 +49,7 @@ type SelectionCacheEntry = Readonly<{ value: unknown }>;
47
49
48
50
interface ReactTimeSyncApi {
49
51
subscribe : ( entry : ReactSubscriptionEntry ) => ( ) => void ;
52
+ getTimeSnapshot : ( ) => Date ;
50
53
getSelectionSnapshot : < T = unknown > ( id : string ) => T ;
51
54
invalidateSelection : ( id : string , select ?: SelectCallback ) => void ;
52
55
}
@@ -63,15 +66,19 @@ class ReactTimeSync implements ReactTimeSyncApi {
63
66
// All functions that are part of the public interface must be defined as
64
67
// arrow functions, so that they work properly with React
65
68
69
+ getTimeSnapshot = ( ) => {
70
+ return this . #timeSync. getTimeSnapshot ( ) ;
71
+ } ;
72
+
66
73
subscribe = ( entry : ReactSubscriptionEntry ) : ( ( ) => void ) => {
67
- const { select, id, idealRefreshIntervalMs , onUpdate } = entry ;
74
+ const { select, id, targetRefreshInterval , onUpdate } = entry ;
68
75
69
76
// Make sure that we subscribe first, in case TimeSync is configured to
70
77
// invalidate the snapshot on a new subscription. Want to remove risk of
71
78
// stale data
72
79
const patchedEntry : SubscriptionEntry = {
73
80
id,
74
- idealRefreshIntervalMs ,
81
+ targetRefreshInterval ,
75
82
onUpdate : ( newDate ) => {
76
83
const prevSelection = this . getSelectionSnapshot ( id ) ;
77
84
const newSelection = select ?.( newDate ) ?? newDate ;
@@ -157,7 +164,15 @@ export const TimeSyncProvider: FC<TimeSyncProviderProps> = ({
157
164
) ;
158
165
} ;
159
166
160
- type UseTimeSyncOptions < T = Date > = Readonly < {
167
+ function useTimeSyncContext ( ) : ReactTimeSync {
168
+ const timeSync = useContext ( timeSyncContext ) ;
169
+ if ( timeSync === null ) {
170
+ throw new Error ( "Cannot call useTimeSync outside of a TimeSyncProvider" ) ;
171
+ }
172
+ return timeSync ;
173
+ }
174
+
175
+ type UseTimeSyncOptions = Readonly < {
161
176
/**
162
177
* targetRefreshInterval is the ideal interval of time, in milliseconds,
163
178
* that defines how often the hook should refresh with the newest Date
@@ -172,26 +187,76 @@ type UseTimeSyncOptions<T = Date> = Readonly<{
172
187
* sync with other useTimeSync users that are currently mounted on screen.
173
188
*/
174
189
targetRefreshInterval : number ;
190
+ } > ;
175
191
176
- /**
177
- * selectDependencies acts like the dependency array for a useMemo callback.
178
- * Whenever any of the elements in the array change by value, that will
179
- * cause the select callback to re-run synchronously and produce a new,
180
- * up-to-date value for the current render.
181
- */
182
- selectDependencies ?: readonly unknown [ ] ;
192
+ export function useTimeSync ( options : UseTimeSyncOptions ) : Date {
193
+ const { targetRefreshInterval } = options ;
194
+ const hookId = useId ( ) ;
195
+ const timeSync = useTimeSyncContext ( ) ;
183
196
184
- /**
185
- * Allows you to transform any date values received from the TimeSync class.
186
- * Select functions work similarly to the selects from React Query and Redux
187
- * Toolkit – you don't need to memoize them, and they will only run when the
188
- * underlying TimeSync state has changed.
189
- *
190
- * Select functions must not be async. The hook will error out at the type
191
- * level if you provide one by mistake.
192
- */
193
- select ?: ( latestDatetime : Date ) => T extends Promise < unknown > ? never : T ;
194
- } > ;
197
+ const subscribe = useCallback < ReactSubscriptionCallback > (
198
+ ( notifyReact ) => {
199
+ return timeSync . subscribe ( {
200
+ targetRefreshInterval,
201
+ id : hookId ,
202
+ onUpdate : notifyReact ,
203
+ } ) ;
204
+ } ,
205
+ [ hookId , timeSync , targetRefreshInterval ] ,
206
+ ) ;
207
+
208
+ const snapshot = useSyncExternalStore ( subscribe , timeSync . getTimeSnapshot ) ;
209
+ return snapshot ;
210
+ }
211
+
212
+ function areDepsInvalidated (
213
+ oldDeps : readonly unknown [ ] | undefined ,
214
+ newDeps : readonly unknown [ ] | undefined ,
215
+ ) : boolean {
216
+ if ( oldDeps === undefined ) {
217
+ if ( newDeps === undefined ) {
218
+ return false ;
219
+ }
220
+ return true ;
221
+ }
222
+
223
+ const oldRecast = oldDeps as readonly unknown [ ] ;
224
+ const newRecast = oldDeps as readonly unknown [ ] ;
225
+ if ( oldRecast . length !== newRecast . length ) {
226
+ return true ;
227
+ }
228
+
229
+ for ( const [ index , el ] of oldRecast . entries ( ) ) {
230
+ if ( el !== newRecast [ index ] ) {
231
+ return true ;
232
+ }
233
+ }
234
+
235
+ return false ;
236
+ }
237
+
238
+ type UseTimeSyncSelectOptions < T = Date > = Readonly <
239
+ UseTimeSyncOptions & {
240
+ /**
241
+ * selectDependencies acts like the dependency array for a useMemo
242
+ * callback. Whenever any of the elements in the array change by value,
243
+ * that will cause the select callback to re-run synchronously and
244
+ * produce a new, up-to-date value for the current render.
245
+ */
246
+ selectDependencies : readonly unknown [ ] ;
247
+
248
+ /**
249
+ * Allows you to transform any date values received from the TimeSync
250
+ * class. Select functions work similarly to the selects from React
251
+ * Query and Redux Toolkit – you don't need to memoize them, and they
252
+ * will only run when the underlying TimeSync state has changed.
253
+ *
254
+ * Select functions must not be async. The hook will error out at the
255
+ * type level if you provide one by mistake.
256
+ */
257
+ select : ( latestDatetime : Date ) => T extends Promise < unknown > ? never : T ;
258
+ }
259
+ > ;
195
260
196
261
/**
197
262
* useTimeSync provides React bindings for the TimeSync class, letting a React
@@ -208,21 +273,12 @@ type UseTimeSyncOptions<T = Date> = Readonly<{
208
273
* frequent update interval, both component instances will update on that
209
274
* interval.
210
275
*/
211
- export function useTimeSync < T = Date > ( options : UseTimeSyncOptions < T > ) : T {
212
- const {
213
- select,
214
- selectDependencies : selectDeps ,
215
- targetRefreshInterval : idealRefreshIntervalMs ,
216
- } = options ;
217
- const timeSync = useContext ( timeSyncContext ) ;
218
- if ( timeSync === null ) {
219
- throw new Error ( "Cannot call useTimeSync outside of a TimeSyncProvider" ) ;
220
- }
221
-
222
- // Abusing useId a little bit here. It's mainly meant to be used for
223
- // accessibility, but it also gives us a globally unique ID associated with
224
- // whichever component instance is consuming this hook
276
+ export function useTimeSyncSelect < T = Date > (
277
+ options : UseTimeSyncSelectOptions < T > ,
278
+ ) : T {
279
+ const { select, selectDependencies, targetRefreshInterval } = options ;
225
280
const hookId = useId ( ) ;
281
+ const timeSync = useTimeSyncContext ( ) ;
226
282
227
283
// This is the one place where we're borderline breaking the React rules.
228
284
// useEffectEvent is meant to be used only in useEffect calls, and normally
@@ -249,21 +305,20 @@ export function useTimeSync<T = Date>(options: UseTimeSyncOptions<T>): T {
249
305
// the new callback). All other values need to be included in the dependency
250
306
// array for correctness, but they should always maintain stable memory
251
307
// addresses
252
- type ReactSubscriptionCallback = ( notifyReact : ( ) => void ) => ( ) => void ;
253
308
const subscribe = useCallback < ReactSubscriptionCallback > (
254
309
( notifyReact ) => {
255
310
return timeSync . subscribe ( {
256
- idealRefreshIntervalMs ,
311
+ targetRefreshInterval : targetRefreshInterval ,
257
312
id : hookId ,
258
313
onUpdate : notifyReact ,
259
314
select : selectForOutsideReact ,
260
315
} ) ;
261
316
} ,
262
- [ timeSync , hookId , selectForOutsideReact , idealRefreshIntervalMs ] ,
317
+ [ timeSync , hookId , selectForOutsideReact , targetRefreshInterval ] ,
263
318
) ;
264
319
265
- const [ prevDeps , setPrevDeps ] = useState ( selectDeps ) ;
266
- const depsAreInvalidated = areDepsInvalidated ( prevDeps , selectDeps ) ;
320
+ const [ prevDeps , setPrevDeps ] = useState ( selectDependencies ) ;
321
+ const depsAreInvalidated = areDepsInvalidated ( prevDeps , selectDependencies ) ;
267
322
268
323
const selection = useSyncExternalStore < T > ( subscribe , ( ) => {
269
324
if ( depsAreInvalidated ) {
@@ -279,34 +334,8 @@ export function useTimeSync<T = Date>(options: UseTimeSyncOptions<T>): T {
279
334
// Setting state mid-render like this is valid, but we just need to make
280
335
// sure that we wait until after the useSyncExternalStore state getter runs
281
336
if ( depsAreInvalidated ) {
282
- setPrevDeps ( selectDeps ) ;
337
+ setPrevDeps ( selectDependencies ) ;
283
338
}
284
339
285
340
return selection ;
286
341
}
287
-
288
- function areDepsInvalidated (
289
- oldDeps : readonly unknown [ ] | undefined ,
290
- newDeps : readonly unknown [ ] | undefined ,
291
- ) : boolean {
292
- if ( oldDeps === undefined ) {
293
- if ( newDeps === undefined ) {
294
- return false ;
295
- }
296
- return true ;
297
- }
298
-
299
- const oldRecast = oldDeps as readonly unknown [ ] ;
300
- const newRecast = oldDeps as readonly unknown [ ] ;
301
- if ( oldRecast . length !== newRecast . length ) {
302
- return true ;
303
- }
304
-
305
- for ( const [ index , el ] of oldRecast . entries ( ) ) {
306
- if ( el !== newRecast [ index ] ) {
307
- return true ;
308
- }
309
- }
310
-
311
- return false ;
312
- }
0 commit comments