@khanacademy/wonder-blocks-timing 4.0.1 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/components/with-action-scheduler.d.ts +4 -1
  3. package/dist/es/index.js +93 -105
  4. package/dist/hooks/use-interval.d.ts +27 -5
  5. package/dist/hooks/use-timeout.d.ts +27 -5
  6. package/dist/index.d.ts +2 -5
  7. package/dist/index.js +92 -105
  8. package/dist/util/animation-frame.d.ts +6 -6
  9. package/dist/util/interval.d.ts +8 -8
  10. package/dist/util/policies.d.ts +12 -8
  11. package/dist/util/timeout.d.ts +8 -7
  12. package/dist/util/types.d.ts +18 -10
  13. package/package.json +3 -3
  14. package/src/components/with-action-scheduler.tsx +9 -1
  15. package/src/hooks/__tests__/use-interval.test.ts +453 -56
  16. package/src/hooks/__tests__/use-timeout.test.ts +412 -58
  17. package/src/hooks/use-interval.ts +90 -25
  18. package/src/hooks/use-timeout.ts +90 -25
  19. package/src/index.ts +4 -14
  20. package/src/util/__tests__/animation-frame.test.ts +9 -10
  21. package/src/util/animation-frame.ts +12 -16
  22. package/src/util/interval.ts +13 -18
  23. package/src/util/policies.ts +13 -8
  24. package/src/util/timeout.ts +14 -18
  25. package/src/util/types.ts +19 -11
  26. package/tsconfig-build.json +2 -4
  27. package/tsconfig-build.tsbuildinfo +1 -1
  28. package/dist/components/action-scheduler-provider.js.flow +0 -33
  29. package/dist/components/with-action-scheduler.js.flow +0 -22
  30. package/dist/hooks/internal/use-updating-ref.d.ts +0 -13
  31. package/dist/hooks/internal/use-updating-ref.js.flow +0 -20
  32. package/dist/hooks/use-interval.js.flow +0 -17
  33. package/dist/hooks/use-scheduled-interval.d.ts +0 -2
  34. package/dist/hooks/use-scheduled-interval.js.flow +0 -12
  35. package/dist/hooks/use-scheduled-timeout.d.ts +0 -2
  36. package/dist/hooks/use-scheduled-timeout.js.flow +0 -12
  37. package/dist/hooks/use-timeout.js.flow +0 -17
  38. package/dist/index.js.flow +0 -31
  39. package/dist/util/action-scheduler.js.flow +0 -38
  40. package/dist/util/animation-frame.js.flow +0 -70
  41. package/dist/util/interval.js.flow +0 -69
  42. package/dist/util/policies.js.flow +0 -14
  43. package/dist/util/timeout.js.flow +0 -71
  44. package/dist/util/types.js.flow +0 -232
  45. package/dist/util/types.typestest.js.flow +0 -6
  46. package/src/hooks/__tests__/use-scheduled-interval.test.ts +0 -460
  47. package/src/hooks/__tests__/use-scheduled-timeout.test.ts +0 -478
  48. package/src/hooks/internal/use-updating-ref.ts +0 -23
  49. package/src/hooks/use-scheduled-interval.ts +0 -72
  50. package/src/hooks/use-scheduled-timeout.ts +0 -79
@@ -1,4 +1,5 @@
1
- import {renderHook} from "@testing-library/react-hooks";
1
+ import {renderHook, act} from "@testing-library/react-hooks";
2
+ import {SchedulePolicy, ClearPolicy, ActionPolicy} from "../../util/policies";
2
3
 
3
4
  import {useTimeout} from "../use-timeout";
4
5
 
@@ -7,129 +8,482 @@ describe("useTimeout", () => {
7
8
  jest.useFakeTimers();
8
9
  });
9
10
 
10
- it("should not fire if 'active' is false", () => {
11
+ afterEach(() => {
12
+ jest.restoreAllMocks();
13
+ });
14
+
15
+ it("throws if the action is not a function", () => {
11
16
  // Arrange
12
- const action = jest.fn();
13
17
 
14
18
  // Act
15
- renderHook(() => useTimeout(action, 500, false));
16
- jest.advanceTimersByTime(501);
19
+ const {result} = renderHook(() => useTimeout(null as any, 1000));
17
20
 
18
21
  // Assert
19
- expect(action).not.toHaveBeenCalled();
22
+ expect(result.error).toEqual(Error("Action must be a function"));
20
23
  });
21
24
 
22
- it("should not fire before the timeout", () => {
25
+ it("throws if the period is less than 0", () => {
23
26
  // Arrange
24
- const action = jest.fn();
25
27
 
26
28
  // Act
27
- renderHook(() => useTimeout(action, 500, true));
28
- jest.advanceTimersByTime(499);
29
+ const {result} = renderHook(() => useTimeout(() => {}, -1));
29
30
 
30
31
  // Assert
31
- expect(action).not.toHaveBeenCalled();
32
+ expect(result.error).toEqual(Error("Timeout period must be >= 0"));
32
33
  });
33
34
 
34
- it("should fire after the timeout", () => {
35
+ it("should return an ITimeout", () => {
35
36
  // Arrange
36
- const action = jest.fn();
37
+ const {result} = renderHook(() => useTimeout(() => {}, 1000));
37
38
 
38
39
  // Act
39
- renderHook(() => useTimeout(action, 500, true));
40
- jest.advanceTimersByTime(501);
41
40
 
42
41
  // Assert
43
- expect(action).toHaveBeenCalledTimes(1);
42
+ expect(result.current).toEqual(
43
+ expect.objectContaining({
44
+ clear: expect.any(Function),
45
+ set: expect.any(Function),
46
+ isSet: expect.any(Boolean),
47
+ }),
48
+ );
49
+ });
50
+
51
+ it("should default to being immediately set", () => {
52
+ // Arrange
53
+ const {result} = renderHook(() => useTimeout(() => {}, 1000));
54
+
55
+ // Act
56
+
57
+ // Assert
58
+ expect(result.current.isSet).toBe(true);
44
59
  });
45
60
 
46
- it("should fire after time after 'active' changes to true", () => {
61
+ it("should call the action before unmounting", () => {
47
62
  // Arrange
48
63
  const action = jest.fn();
49
- const {rerender} = renderHook(
50
- ({active}: any) => useTimeout(action, 500, active),
51
- {initialProps: {active: false}},
64
+ const {unmount} = renderHook(() =>
65
+ useTimeout(action, 1000, {
66
+ clearPolicy: ClearPolicy.Resolve,
67
+ }),
52
68
  );
53
69
 
54
70
  // Act
55
- rerender({active: true});
56
- jest.advanceTimersByTime(501);
71
+ act(() => {
72
+ unmount();
73
+ });
57
74
 
58
75
  // Assert
59
76
  expect(action).toHaveBeenCalled();
60
77
  });
61
78
 
62
- it("should not fire after the timeout if 'active' changes to false", () => {
79
+ it("should call the current action", () => {
63
80
  // Arrange
64
- const action = jest.fn();
81
+ const action1 = jest.fn();
82
+ const action2 = jest.fn();
65
83
  const {rerender} = renderHook(
66
- ({active}: any) => useTimeout(action, 500, active),
67
- {initialProps: {active: true}},
84
+ ({action}: any) => useTimeout(action, 500),
85
+ {
86
+ initialProps: {action: action1},
87
+ },
68
88
  );
69
89
 
70
90
  // Act
71
- rerender({active: false});
91
+ rerender({action: action2});
72
92
  jest.advanceTimersByTime(501);
73
93
 
74
94
  // Assert
75
- expect(action).not.toHaveBeenCalled();
95
+ expect(action2).toHaveBeenCalledTimes(1);
76
96
  });
77
97
 
78
- it("should reset the timeout if 'timeoutMs' is changes", () => {
98
+ it("should only call setTimeout once even if action changes", () => {
79
99
  // Arrange
80
- const action = jest.fn();
81
-
82
- // Act
100
+ const timeoutSpy = jest.spyOn(global, "setTimeout");
101
+ const action1 = jest.fn();
102
+ const action2 = jest.fn();
83
103
  const {rerender} = renderHook(
84
- ({timeoutMs}: any) => useTimeout(action, timeoutMs, true),
85
- {initialProps: {timeoutMs: 500}},
104
+ ({action}: any) => useTimeout(action, 500),
105
+ {
106
+ initialProps: {action: action1},
107
+ },
86
108
  );
87
- rerender({timeoutMs: 1000});
88
- jest.advanceTimersByTime(501);
109
+ // Act
110
+ rerender({action: action2});
89
111
 
90
112
  // Assert
91
- expect(action).not.toHaveBeenCalled();
92
- jest.advanceTimersByTime(1001);
93
- expect(action).toHaveBeenCalled();
113
+ expect(timeoutSpy).toHaveBeenCalledOnce();
94
114
  });
95
115
 
96
- it("should not reset the timeout if 'action' changes", () => {
116
+ it("should not reset the timeout if the action changes", () => {
97
117
  // Arrange
98
118
  const action1 = jest.fn();
99
119
  const action2 = jest.fn();
100
- const timeoutSpy = jest.spyOn(window, "setTimeout");
101
-
102
- // Act
103
120
  const {rerender} = renderHook(
104
- ({action}: any) => useTimeout(action, 500, true),
105
- {initialProps: {action: action1}},
121
+ ({action}: any) => useTimeout(action, 500),
122
+ {
123
+ initialProps: {action: action1},
124
+ },
106
125
  );
107
- // NOTE: For some reason setTimeout is called twice by the time we get
108
- // here. I've verified that it only gets called once inside the hook
109
- // so something else must be calling it.
110
- const callCount = timeoutSpy.mock.calls.length;
126
+
127
+ // Act
128
+ jest.advanceTimersByTime(250);
111
129
  rerender({action: action2});
112
- jest.advanceTimersByTime(501);
130
+ jest.advanceTimersByTime(251);
113
131
 
114
132
  // Assert
115
- expect(timeoutSpy).toHaveBeenCalledTimes(callCount);
133
+ expect(action1).not.toHaveBeenCalled();
134
+ expect(action2).toHaveBeenCalledTimes(1);
116
135
  });
117
136
 
118
- it("should fire the current action if 'action' changes", () => {
137
+ it("should reset the timeout if the action changes and the action policy is Reset", () => {
119
138
  // Arrange
120
139
  const action1 = jest.fn();
121
140
  const action2 = jest.fn();
141
+ const {rerender} = renderHook(
142
+ ({action}: any) =>
143
+ useTimeout(action, 500, {actionPolicy: ActionPolicy.Reset}),
144
+ {
145
+ initialProps: {action: action1},
146
+ },
147
+ );
122
148
 
123
149
  // Act
150
+ jest.advanceTimersByTime(250);
151
+ rerender({action: action2});
152
+ jest.advanceTimersByTime(251);
153
+
154
+ // Assert
155
+ expect(action1).not.toHaveBeenCalled();
156
+ expect(action2).not.toHaveBeenCalled();
157
+ });
158
+
159
+ it("should use the new timeout duration after changing it", () => {
160
+ // Arrange
161
+ const action = jest.fn();
124
162
  const {rerender} = renderHook(
125
- ({action}: any) => useTimeout(action, 500, true),
126
- {initialProps: {action: action1}},
163
+ ({timeoutMs}: any) => useTimeout(action, timeoutMs),
164
+ {
165
+ initialProps: {timeoutMs: 500},
166
+ },
167
+ );
168
+ rerender({timeoutMs: 1000});
169
+
170
+ // Act
171
+ jest.advanceTimersByTime(1501);
172
+
173
+ // Assert
174
+ expect(action).toHaveBeenCalledTimes(1);
175
+ });
176
+
177
+ it("should restart the timeout if intervalMs changes", () => {
178
+ // Arrange
179
+ const timeoutSpy = jest.spyOn(global, "setTimeout");
180
+ const {rerender} = renderHook(
181
+ ({timeoutMs}: any) => useTimeout(() => {}, timeoutMs),
182
+ {
183
+ initialProps: {timeoutMs: 500},
184
+ },
127
185
  );
128
- rerender({action: action2});
129
- jest.advanceTimersByTime(501);
186
+
187
+ // Act
188
+ rerender({timeoutMs: 1000});
130
189
 
131
190
  // Assert
132
- expect(action1).not.toHaveBeenCalledWith();
133
- expect(action2).toHaveBeenCalledWith();
191
+ expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), 500);
192
+ expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), 1000);
193
+ });
194
+
195
+ describe("SchedulePolicy.Immediately", () => {
196
+ it("should call the action after the timeout expires", () => {
197
+ // Arrange
198
+ const action = jest.fn();
199
+ renderHook(() => useTimeout(action, 1000));
200
+
201
+ // Act
202
+ act(() => {
203
+ jest.advanceTimersByTime(1000);
204
+ });
205
+
206
+ // Assert
207
+ expect(action).toHaveBeenCalled();
208
+ });
209
+
210
+ it("should update isSet to false after the timeout expires", () => {
211
+ // Arrange
212
+ const action = jest.fn();
213
+ const {result} = renderHook(() => useTimeout(action, 1000));
214
+
215
+ // Act
216
+ act(() => {
217
+ jest.advanceTimersByTime(1001);
218
+ });
219
+
220
+ // Assert
221
+ expect(result.current.isSet).toBe(false);
222
+ });
223
+
224
+ it("should call the action again if 'set' is called after the action was called", () => {
225
+ // Arrange
226
+ const action = jest.fn();
227
+ const {result} = renderHook(() => useTimeout(action, 1000));
228
+
229
+ // Act
230
+ act(() => {
231
+ jest.advanceTimersByTime(1001);
232
+ });
233
+ act(() => {
234
+ result.current.set();
235
+ });
236
+ act(() => {
237
+ jest.advanceTimersByTime(1001);
238
+ });
239
+
240
+ // Assert
241
+ expect(action).toHaveBeenCalledTimes(2);
242
+ });
243
+
244
+ it("should restart the timeout if timeoutMs gets updated", () => {
245
+ // Arrange
246
+ const action = jest.fn();
247
+ const {rerender} = renderHook(
248
+ ({timeoutMs}: any) => useTimeout(action, timeoutMs),
249
+ {
250
+ initialProps: {timeoutMs: 1000},
251
+ },
252
+ );
253
+
254
+ // Act
255
+ act(() => {
256
+ jest.advanceTimersByTime(900);
257
+ });
258
+ rerender({timeoutMs: 500});
259
+ act(() => {
260
+ jest.advanceTimersByTime(100);
261
+ });
262
+
263
+ // Assert
264
+ expect(action).not.toHaveBeenCalled();
265
+ act((): void => jest.advanceTimersByTime(500));
266
+ expect(action).toHaveBeenCalled();
267
+ });
268
+
269
+ it("should should timeout after the new timeoutMs if it gets updated", () => {
270
+ // Arrange
271
+ const action = jest.fn();
272
+ const {rerender} = renderHook(
273
+ ({timeoutMs}: any) => useTimeout(action, timeoutMs),
274
+ {
275
+ initialProps: {timeoutMs: 1000},
276
+ },
277
+ );
278
+
279
+ // Act
280
+ rerender({timeoutMs: 500});
281
+ act(() => {
282
+ jest.advanceTimersByTime(500);
283
+ });
284
+
285
+ // Assert
286
+ expect(action).toHaveBeenCalled();
287
+ });
288
+
289
+ it("should call the new action after re-rendering with a new action", () => {
290
+ // Arrange
291
+ const action1 = jest.fn();
292
+ const action2 = jest.fn();
293
+ const {rerender} = renderHook(
294
+ ({action}: any) => useTimeout(action, 1000),
295
+ {
296
+ initialProps: {action: action1},
297
+ },
298
+ );
299
+
300
+ // Act
301
+ rerender({action: action2});
302
+ act(() => {
303
+ jest.advanceTimersByTime(1000);
304
+ });
305
+
306
+ // Assert
307
+ expect(action2).toHaveBeenCalled();
308
+ });
309
+
310
+ it("should not call the original action after re-rendering with a new action", () => {
311
+ // Arrange
312
+ const action1 = jest.fn();
313
+ const action2 = jest.fn();
314
+ const {rerender} = renderHook(
315
+ ({action}: any) => useTimeout(action, 1000),
316
+ {
317
+ initialProps: {action: action1},
318
+ },
319
+ );
320
+
321
+ // Act
322
+ rerender({action: action2});
323
+ act(() => {
324
+ jest.advanceTimersByTime(1000);
325
+ });
326
+
327
+ // Assert
328
+ expect(action1).not.toHaveBeenCalled();
329
+ });
330
+
331
+ it("should not call the action if the timeout is cleared", () => {
332
+ // Arrange
333
+ const action = jest.fn();
334
+ const {result} = renderHook(() => useTimeout(action, 1000));
335
+
336
+ // Act
337
+ act(() => {
338
+ result.current.clear();
339
+ });
340
+ act(() => {
341
+ jest.advanceTimersByTime(1000);
342
+ });
343
+
344
+ // Assert
345
+ expect(action).not.toHaveBeenCalled();
346
+ });
347
+
348
+ it("should call the action when the timeout is cleared when passing ClearPolicy.Resolve to clear()", () => {
349
+ // Arrange
350
+ const action = jest.fn();
351
+ const {result} = renderHook(() => useTimeout(action, 1000));
352
+
353
+ // Act
354
+ act(() => {
355
+ result.current.clear(ClearPolicy.Resolve);
356
+ });
357
+
358
+ // Assert
359
+ expect(action).toHaveBeenCalled();
360
+ });
361
+
362
+ it("should call the action on unmount when using ClearPolicy.Resolve in options", () => {
363
+ // Arrange
364
+ const action = jest.fn();
365
+ const {unmount} = renderHook(() =>
366
+ useTimeout(action, 1000, {
367
+ clearPolicy: ClearPolicy.Resolve,
368
+ }),
369
+ );
370
+
371
+ // Act
372
+ unmount();
373
+
374
+ // Assert
375
+ expect(action).toHaveBeenCalled();
376
+ });
377
+
378
+ it("should not call the action on unmount when using the default options", () => {
379
+ // Arrange
380
+ const action = jest.fn();
381
+ const {unmount} = renderHook(() => useTimeout(action, 1000));
382
+
383
+ // Act
384
+ unmount();
385
+
386
+ // Assert
387
+ expect(action).not.toHaveBeenCalled();
388
+ });
389
+ });
390
+
391
+ describe("SchedulePolicy.OnDemand", () => {
392
+ it("should not set the timer on creation", () => {
393
+ // Arrange
394
+ const {result} = renderHook(() =>
395
+ useTimeout(() => {}, 1000, {
396
+ schedulePolicy: SchedulePolicy.OnDemand,
397
+ }),
398
+ );
399
+
400
+ // Act
401
+
402
+ // Assert
403
+ expect(result.current.isSet).toBe(false);
404
+ });
405
+
406
+ it("should not call action after timeoutMs if the timer hasn't been set", () => {
407
+ // Arrange
408
+ const action = jest.fn();
409
+ renderHook(() =>
410
+ useTimeout(action, 1000, {
411
+ schedulePolicy: SchedulePolicy.OnDemand,
412
+ }),
413
+ );
414
+
415
+ // Act
416
+ act(() => {
417
+ jest.advanceTimersByTime(1000);
418
+ });
419
+
420
+ // Assert
421
+ expect(action).not.toHaveBeenCalled();
422
+ });
423
+
424
+ it("should call action after timeoutMs if the timer has been set", () => {
425
+ // Arrange
426
+ const action = jest.fn();
427
+ const {result} = renderHook(() =>
428
+ useTimeout(action, 1000, {
429
+ schedulePolicy: SchedulePolicy.OnDemand,
430
+ }),
431
+ );
432
+
433
+ // Act
434
+ act(() => {
435
+ result.current.set();
436
+ });
437
+ act(() => {
438
+ jest.advanceTimersByTime(1000);
439
+ });
440
+
441
+ // Assert
442
+ expect(action).toHaveBeenCalled();
443
+ });
444
+
445
+ it("should reset the timer after calling set() again", () => {
446
+ // Arrange
447
+ const action = jest.fn();
448
+ const {result} = renderHook(() =>
449
+ useTimeout(action, 750, {
450
+ schedulePolicy: SchedulePolicy.OnDemand,
451
+ }),
452
+ );
453
+
454
+ // Act
455
+ act(() => {
456
+ result.current.set();
457
+ jest.advanceTimersByTime(501);
458
+ result.current.set();
459
+ jest.advanceTimersByTime(501);
460
+ });
461
+
462
+ // Assert
463
+ expect(action).not.toHaveBeenCalled();
464
+ });
465
+
466
+ it("should call the action after calling set() again", () => {
467
+ // Arrange
468
+ const action = jest.fn();
469
+ const {result} = renderHook(() =>
470
+ useTimeout(action, 500, {
471
+ schedulePolicy: SchedulePolicy.OnDemand,
472
+ }),
473
+ );
474
+
475
+ // Act
476
+ act(() => {
477
+ result.current.set();
478
+ jest.advanceTimersByTime(501);
479
+ });
480
+ act(() => {
481
+ result.current.set();
482
+ jest.advanceTimersByTime(501);
483
+ });
484
+
485
+ // Assert
486
+ expect(action).toHaveBeenCalledTimes(2);
487
+ });
134
488
  });
135
489
  });
@@ -1,36 +1,101 @@
1
- import {useEffect} from "react";
1
+ import {useEffect, useMemo, useRef} from "react";
2
+ import {ClearPolicy, ActionPolicy} from "../util/policies";
2
3
 
3
- import {useUpdatingRef} from "./internal/use-updating-ref";
4
+ import type {IInterval, HookOptions} from "../util/types";
5
+
6
+ import Interval from "../util/interval";
4
7
 
5
8
  /**
6
- * A simple hook for using `setInterval`.
9
+ * Hook providing access to a scheduled interval.
7
10
  *
8
- * @param action called every `intervalMs` when `active` is true
9
- * @param intervalMs the duration between calls to `action`
10
- * @param active whether or not the interval is active
11
+ * @param action The action to be invoked each time the interval period has
12
+ * passed. By default, this will not cause the interval to restart if it
13
+ * changes. This makes it easier to use with inline lambda functions rather than
14
+ * requiring consumers to wrap their action in a `useCallback`. To change this
15
+ * behavior, see the `actionPolicy` option.
16
+ * @param intervalMs The interval period. If this changes, the interval will
17
+ * be reset per the `schedulePolicy` option.
18
+ * @param options Options for the hook.
19
+ * @param options.actionPolicy Determines how the action is handled when it
20
+ * changes. By default, the action is replaced but the interval is not reset,
21
+ * and the updated action will be invoked when the interval next fires.
22
+ * If you want to reset the interval when the action changes, use
23
+ * `ActionPolicy.Reset`.
24
+ * @param options.clearPolicy Determines how the interval is cleared when the
25
+ * component is unmounted or the interval is recreated. By default, the
26
+ * interval is cleared immediately. If you want to let the interval run to
27
+ * completion, use `ClearPolicy.Resolve`. This is NOT applied if the interval
28
+ * is cleared manually via the `clear()` method on the returned API.
29
+ * @param options.schedulePolicy Determines when the interval is scheduled.
30
+ * By default, the interval is scheduled immediately. If you want to delay
31
+ * scheduling the interval, use `SchedulePolicy.OnDemand`.
32
+ * @returns An `IInterval` API for interacting with the given interval. This
33
+ * API is a no-op if called when not mounted. This means that any calls prior
34
+ * to mounting or after unmounting will not have any effect.
11
35
  */
12
36
  export function useInterval(
13
37
  action: () => unknown,
14
38
  intervalMs: number,
15
- active: boolean,
16
- ) {
17
- // We using a ref instead of a callback for `action` to avoid resetting
18
- // the interval whenever the `action` changes.
19
- const actionRef = useUpdatingRef(action);
39
+ options: HookOptions = {},
40
+ ): IInterval {
41
+ const {actionPolicy, clearPolicy, schedulePolicy} = options;
42
+ const actionProxyRef = useRef<() => unknown>(action);
43
+ const intervalRef = useRef<IInterval | null>(null);
20
44
 
21
- useEffect(() => {
22
- if (active) {
23
- const intervalId = setInterval(() => {
24
- actionRef.current();
25
- }, intervalMs);
26
-
27
- return () => {
28
- clearInterval(intervalId);
29
- };
45
+ // Since we are passing our proxy function to the interval instance,
46
+ // it's check that the action is a function will never fail. So, we have to
47
+ // do that check ourselves, and we do it here.
48
+ if (typeof action !== "function") {
49
+ throw new Error("Action must be a function");
50
+ }
51
+
52
+ // If we're rendered with an updated action, we want to update the ref
53
+ // so the existing interval gets the new action, and then reset the
54
+ // interval if our action policy calls for it.
55
+ if (action !== actionProxyRef.current) {
56
+ actionProxyRef.current = action;
57
+ if (actionPolicy === ActionPolicy.Reset) {
58
+ intervalRef.current?.set();
30
59
  }
31
- // actionRef isn't actually required, but react-hooks/exhaustive-deps
32
- // doesn't recognize it as a ref and thus complains if it isn't in the
33
- // deps list. It isn't a big deal though since the value ofactionRef
34
- // never changes (only its contents do).
35
- }, [intervalMs, active, actionRef]);
60
+ }
61
+
62
+ // This effect updates the interval when the intervalMs, clearPolicy,
63
+ // or schedulePolicy changes.
64
+ useEffect(() => {
65
+ // Make a new interval.
66
+ intervalRef.current = new Interval(
67
+ () => {
68
+ actionProxyRef.current?.();
69
+ },
70
+ intervalMs,
71
+ schedulePolicy,
72
+ );
73
+
74
+ // Clear the interval when the effect is cleaned up, if necessary,
75
+ // making sure to use the clear policy.
76
+ return () => {
77
+ intervalRef.current?.clear(clearPolicy);
78
+ intervalRef.current = null;
79
+ };
80
+ }, [intervalMs, clearPolicy, schedulePolicy]);
81
+
82
+ // This is the API we expose to the consumer. We expose this rather than
83
+ // the interval instance itself so that the API we give back is stable
84
+ // even if the underlying interval instance changes.
85
+ const externalApi = useMemo(
86
+ () => ({
87
+ set: () => {
88
+ intervalRef.current?.set();
89
+ },
90
+ clear: (policy?: ClearPolicy) => {
91
+ intervalRef.current?.clear(policy);
92
+ },
93
+ get isSet() {
94
+ return intervalRef.current?.isSet ?? false;
95
+ },
96
+ }),
97
+ [],
98
+ );
99
+
100
+ return externalApi;
36
101
  }