@@ -25,12 +25,14 @@ import {
2525import { useEffectEvent } from "./hookPolyfills" ;
2626
2727export {
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 ,
3232} from "utils/TimeSync" ;
3333
34+ type ReactSubscriptionCallback = ( notifyReact : ( ) => void ) => ( ) => void ;
35+
3436type SelectCallback = ( newSnapshot : Date ) => unknown ;
3537
3638type ReactSubscriptionEntry = Readonly <
@@ -47,6 +49,7 @@ type SelectionCacheEntry = Readonly<{ value: unknown }>;
4749
4850interface ReactTimeSyncApi {
4951 subscribe : ( entry : ReactSubscriptionEntry ) => ( ) => void ;
52+ getTimeSnapshot : ( ) => Date ;
5053 getSelectionSnapshot : < T = unknown > ( id : string ) => T ;
5154 invalidateSelection : ( id : string , select ?: SelectCallback ) => void ;
5255}
@@ -63,15 +66,19 @@ class ReactTimeSync implements ReactTimeSyncApi {
6366 // All functions that are part of the public interface must be defined as
6467 // arrow functions, so that they work properly with React
6568
69+ getTimeSnapshot = ( ) => {
70+ return this . #timeSync. getTimeSnapshot ( ) ;
71+ } ;
72+
6673 subscribe = ( entry : ReactSubscriptionEntry ) : ( ( ) => void ) => {
67- const { select, id, idealRefreshIntervalMs , onUpdate } = entry ;
74+ const { select, id, targetRefreshInterval , onUpdate } = entry ;
6875
6976 // Make sure that we subscribe first, in case TimeSync is configured to
7077 // invalidate the snapshot on a new subscription. Want to remove risk of
7178 // stale data
7279 const patchedEntry : SubscriptionEntry = {
7380 id,
74- idealRefreshIntervalMs ,
81+ targetRefreshInterval ,
7582 onUpdate : ( newDate ) => {
7683 const prevSelection = this . getSelectionSnapshot ( id ) ;
7784 const newSelection = select ?.( newDate ) ?? newDate ;
@@ -157,7 +164,15 @@ export const TimeSyncProvider: FC<TimeSyncProviderProps> = ({
157164 ) ;
158165} ;
159166
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 < {
161176 /**
162177 * targetRefreshInterval is the ideal interval of time, in milliseconds,
163178 * that defines how often the hook should refresh with the newest Date
@@ -172,26 +187,76 @@ type UseTimeSyncOptions<T = Date> = Readonly<{
172187 * sync with other useTimeSync users that are currently mounted on screen.
173188 */
174189 targetRefreshInterval : number ;
190+ } > ;
175191
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 ( ) ;
183196
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+ > ;
195260
196261/**
197262 * useTimeSync provides React bindings for the TimeSync class, letting a React
@@ -208,21 +273,12 @@ type UseTimeSyncOptions<T = Date> = Readonly<{
208273 * frequent update interval, both component instances will update on that
209274 * interval.
210275 */
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 ;
225280 const hookId = useId ( ) ;
281+ const timeSync = useTimeSyncContext ( ) ;
226282
227283 // This is the one place where we're borderline breaking the React rules.
228284 // 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 {
249305 // the new callback). All other values need to be included in the dependency
250306 // array for correctness, but they should always maintain stable memory
251307 // addresses
252- type ReactSubscriptionCallback = ( notifyReact : ( ) => void ) => ( ) => void ;
253308 const subscribe = useCallback < ReactSubscriptionCallback > (
254309 ( notifyReact ) => {
255310 return timeSync . subscribe ( {
256- idealRefreshIntervalMs ,
311+ targetRefreshInterval : targetRefreshInterval ,
257312 id : hookId ,
258313 onUpdate : notifyReact ,
259314 select : selectForOutsideReact ,
260315 } ) ;
261316 } ,
262- [ timeSync , hookId , selectForOutsideReact , idealRefreshIntervalMs ] ,
317+ [ timeSync , hookId , selectForOutsideReact , targetRefreshInterval ] ,
263318 ) ;
264319
265- const [ prevDeps , setPrevDeps ] = useState ( selectDeps ) ;
266- const depsAreInvalidated = areDepsInvalidated ( prevDeps , selectDeps ) ;
320+ const [ prevDeps , setPrevDeps ] = useState ( selectDependencies ) ;
321+ const depsAreInvalidated = areDepsInvalidated ( prevDeps , selectDependencies ) ;
267322
268323 const selection = useSyncExternalStore < T > ( subscribe , ( ) => {
269324 if ( depsAreInvalidated ) {
@@ -279,34 +334,8 @@ export function useTimeSync<T = Date>(options: UseTimeSyncOptions<T>): T {
279334 // Setting state mid-render like this is valid, but we just need to make
280335 // sure that we wait until after the useSyncExternalStore state getter runs
281336 if ( depsAreInvalidated ) {
282- setPrevDeps ( selectDeps ) ;
337+ setPrevDeps ( selectDependencies ) ;
283338 }
284339
285340 return selection ;
286341}
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