@khanacademy/wonder-blocks-timing 2.0.3 → 2.1.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.
- package/CHANGELOG.md +11 -0
- package/dist/es/index.js +167 -37
- package/dist/index.js +241 -50
- package/package.json +2 -3
- package/src/__tests__/generated-snapshot.test.js +2 -3
- package/src/components/__tests__/action-scheduler-provider.test.js +1 -0
- package/src/components/__tests__/with-action-scheduler.test.js +1 -0
- package/src/hooks/__tests__/use-interval.test.js +124 -0
- package/src/hooks/__tests__/use-scheduled-interval.test.js +453 -0
- package/src/hooks/__tests__/use-scheduled-timeout.test.js +469 -0
- package/src/hooks/__tests__/use-timeout.test.js +93 -293
- package/src/hooks/internal/use-updating-ref.js +20 -0
- package/src/hooks/use-interval.js +37 -0
- package/src/hooks/use-interval.stories.mdx +80 -0
- package/src/hooks/use-scheduled-interval.js +73 -0
- package/src/hooks/use-scheduled-interval.stories.mdx +147 -0
- package/src/hooks/use-scheduled-timeout.js +80 -0
- package/src/hooks/use-scheduled-timeout.stories.mdx +148 -0
- package/src/hooks/use-timeout.js +22 -55
- package/src/hooks/use-timeout.stories.mdx +24 -96
- package/src/index.js +3 -0
- package/LICENSE +0 -21
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// @flow
|
|
2
|
-
import {renderHook
|
|
3
|
-
import {SchedulePolicy, ClearPolicy} from "../../util/policies.js";
|
|
2
|
+
import {renderHook} from "@testing-library/react-hooks";
|
|
4
3
|
|
|
5
4
|
import {useTimeout} from "../use-timeout.js";
|
|
6
5
|
|
|
@@ -9,328 +8,129 @@ describe("useTimeout", () => {
|
|
|
9
8
|
jest.useFakeTimers();
|
|
10
9
|
});
|
|
11
10
|
|
|
12
|
-
it("should
|
|
11
|
+
it("should not fire if 'active' is false", () => {
|
|
13
12
|
// Arrange
|
|
14
|
-
const
|
|
13
|
+
const action = jest.fn();
|
|
15
14
|
|
|
16
15
|
// Act
|
|
16
|
+
renderHook(() => useTimeout(action, 500, false));
|
|
17
|
+
jest.advanceTimersByTime(501);
|
|
17
18
|
|
|
18
19
|
// Assert
|
|
19
|
-
expect(
|
|
20
|
-
expect.objectContaining({
|
|
21
|
-
clear: expect.any(Function),
|
|
22
|
-
set: expect.any(Function),
|
|
23
|
-
isSet: expect.any(Boolean),
|
|
24
|
-
}),
|
|
25
|
-
);
|
|
20
|
+
expect(action).not.toHaveBeenCalled();
|
|
26
21
|
});
|
|
27
22
|
|
|
28
|
-
it("should
|
|
23
|
+
it("should not fire before the timeout", () => {
|
|
29
24
|
// Arrange
|
|
30
|
-
const
|
|
25
|
+
const action = jest.fn();
|
|
31
26
|
|
|
32
27
|
// Act
|
|
28
|
+
renderHook(() => useTimeout(action, 500, true));
|
|
29
|
+
jest.advanceTimersByTime(499);
|
|
33
30
|
|
|
34
31
|
// Assert
|
|
35
|
-
expect(
|
|
32
|
+
expect(action).not.toHaveBeenCalled();
|
|
36
33
|
});
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const action = jest.fn();
|
|
42
|
-
renderHook(() => useTimeout(action, 1000));
|
|
43
|
-
|
|
44
|
-
// Act
|
|
45
|
-
act(() => {
|
|
46
|
-
jest.advanceTimersByTime(1000);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
// Assert
|
|
50
|
-
expect(action).toHaveBeenCalled();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("should update isSet to false after the timeout expires", () => {
|
|
54
|
-
// Arrange
|
|
55
|
-
const action = jest.fn();
|
|
56
|
-
const {result} = renderHook(() => useTimeout(action, 1000));
|
|
57
|
-
|
|
58
|
-
// Act
|
|
59
|
-
act(() => {
|
|
60
|
-
jest.advanceTimersByTime(1000);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// Assert
|
|
64
|
-
expect(result.current.isSet).toBe(false);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("should call the action again if 'set' is called after the action was called", () => {
|
|
68
|
-
// Arrange
|
|
69
|
-
const action = jest.fn();
|
|
70
|
-
const {result} = renderHook(() => useTimeout(action, 1000));
|
|
71
|
-
|
|
72
|
-
// Act
|
|
73
|
-
act(() => {
|
|
74
|
-
jest.advanceTimersByTime(1001);
|
|
75
|
-
result.current.set();
|
|
76
|
-
jest.advanceTimersByTime(1001);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// Assert
|
|
80
|
-
expect(action).toHaveBeenCalledTimes(2);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("should restart the timeout if timeoutMs gets updated", () => {
|
|
84
|
-
// Arrange
|
|
85
|
-
const action = jest.fn();
|
|
86
|
-
const {rerender} = renderHook(
|
|
87
|
-
({timeoutMs}) => useTimeout(action, timeoutMs),
|
|
88
|
-
{
|
|
89
|
-
initialProps: {timeoutMs: 1000},
|
|
90
|
-
},
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
// Act
|
|
94
|
-
act(() => {
|
|
95
|
-
jest.advanceTimersByTime(900);
|
|
96
|
-
});
|
|
97
|
-
rerender({timeoutMs: 500});
|
|
98
|
-
act(() => {
|
|
99
|
-
jest.advanceTimersByTime(100);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// Assert
|
|
103
|
-
expect(action).not.toHaveBeenCalled();
|
|
104
|
-
act(() => jest.advanceTimersByTime(500));
|
|
105
|
-
expect(action).toHaveBeenCalled();
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("should should timeout after the new timeoutMs if it gets updated", () => {
|
|
109
|
-
// Arrange
|
|
110
|
-
const action = jest.fn();
|
|
111
|
-
const {rerender} = renderHook(
|
|
112
|
-
({timeoutMs}) => useTimeout(action, timeoutMs),
|
|
113
|
-
{
|
|
114
|
-
initialProps: {timeoutMs: 1000},
|
|
115
|
-
},
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
// Act
|
|
119
|
-
rerender({timeoutMs: 500});
|
|
120
|
-
act(() => {
|
|
121
|
-
jest.advanceTimersByTime(500);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// Assert
|
|
125
|
-
expect(action).toHaveBeenCalled();
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("should call the new action after re-rendering with a new action", () => {
|
|
129
|
-
// Arrange
|
|
130
|
-
const action1 = jest.fn();
|
|
131
|
-
const action2 = jest.fn();
|
|
132
|
-
const {rerender} = renderHook(
|
|
133
|
-
({action}) => useTimeout(action, 1000),
|
|
134
|
-
{
|
|
135
|
-
initialProps: {action: action1},
|
|
136
|
-
},
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
// Act
|
|
140
|
-
rerender({action: action2});
|
|
141
|
-
act(() => {
|
|
142
|
-
jest.advanceTimersByTime(1000);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// Assert
|
|
146
|
-
expect(action2).toHaveBeenCalled();
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("should not call the original action after re-rendering with a new action", () => {
|
|
150
|
-
// Arrange
|
|
151
|
-
const action1 = jest.fn();
|
|
152
|
-
const action2 = jest.fn();
|
|
153
|
-
const {rerender} = renderHook(
|
|
154
|
-
({action}) => useTimeout(action, 1000),
|
|
155
|
-
{
|
|
156
|
-
initialProps: {action: action1},
|
|
157
|
-
},
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
// Act
|
|
161
|
-
rerender({action: action2});
|
|
162
|
-
act(() => {
|
|
163
|
-
jest.advanceTimersByTime(1000);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// Assert
|
|
167
|
-
expect(action1).not.toHaveBeenCalled();
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it("should not call the action if the timeout is cleared", () => {
|
|
171
|
-
// Arrange
|
|
172
|
-
const action = jest.fn();
|
|
173
|
-
const {result} = renderHook(() => useTimeout(action, 1000));
|
|
174
|
-
|
|
175
|
-
// Act
|
|
176
|
-
act(() => {
|
|
177
|
-
result.current.clear();
|
|
178
|
-
jest.advanceTimersByTime(1000);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
// Assert
|
|
182
|
-
expect(action).not.toHaveBeenCalled();
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it("should call the action when the timeout is cleared when passing ClearPolicies.Resolve to clear()", () => {
|
|
186
|
-
// Arrange
|
|
187
|
-
const action = jest.fn();
|
|
188
|
-
const {result} = renderHook(() => useTimeout(action, 1000));
|
|
189
|
-
|
|
190
|
-
// Act
|
|
191
|
-
act(() => {
|
|
192
|
-
result.current.clear(ClearPolicy.Resolve);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
// Assert
|
|
196
|
-
expect(action).toHaveBeenCalled();
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it("should call the action when the timeout is cleared when using ClearPolicies.Resolve in options", () => {
|
|
200
|
-
// Arrange
|
|
201
|
-
const action = jest.fn();
|
|
202
|
-
const {result} = renderHook(() =>
|
|
203
|
-
useTimeout(action, 1000, {clearPolicy: ClearPolicy.Resolve}),
|
|
204
|
-
);
|
|
205
|
-
|
|
206
|
-
// Act
|
|
207
|
-
act(() => {
|
|
208
|
-
result.current.clear();
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
// Assert
|
|
212
|
-
expect(action).toHaveBeenCalled();
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it("should call the action on unmount when using ClearPolicies.Resolve in options", () => {
|
|
216
|
-
// Arrange
|
|
217
|
-
const action = jest.fn();
|
|
218
|
-
const {unmount} = renderHook(() =>
|
|
219
|
-
useTimeout(action, 1000, {clearPolicy: ClearPolicy.Resolve}),
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
// Act
|
|
223
|
-
unmount();
|
|
224
|
-
|
|
225
|
-
// Assert
|
|
226
|
-
expect(action).toHaveBeenCalled();
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it("should not call the action on unmount when using the default options", () => {
|
|
230
|
-
// Arrange
|
|
231
|
-
const action = jest.fn();
|
|
232
|
-
const {unmount} = renderHook(() => useTimeout(action, 1000));
|
|
35
|
+
it("should fire after the timeout", () => {
|
|
36
|
+
// Arrange
|
|
37
|
+
const action = jest.fn();
|
|
233
38
|
|
|
234
|
-
|
|
235
|
-
|
|
39
|
+
// Act
|
|
40
|
+
renderHook(() => useTimeout(action, 500, true));
|
|
41
|
+
jest.advanceTimersByTime(501);
|
|
236
42
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
});
|
|
43
|
+
// Assert
|
|
44
|
+
expect(action).toHaveBeenCalledTimes(1);
|
|
240
45
|
});
|
|
241
46
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
);
|
|
47
|
+
it("should fire after time after 'active' changes to true", () => {
|
|
48
|
+
// Arrange
|
|
49
|
+
const action = jest.fn();
|
|
50
|
+
const {rerender} = renderHook(
|
|
51
|
+
({active}) => useTimeout(action, 500, active),
|
|
52
|
+
{initialProps: {active: false}},
|
|
53
|
+
);
|
|
250
54
|
|
|
251
|
-
|
|
55
|
+
// Act
|
|
56
|
+
rerender({active: true});
|
|
57
|
+
jest.advanceTimersByTime(501);
|
|
252
58
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
59
|
+
// Assert
|
|
60
|
+
expect(action).toHaveBeenCalled();
|
|
61
|
+
});
|
|
256
62
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
);
|
|
63
|
+
it("should not fire after the timeout if 'active' changes to false", () => {
|
|
64
|
+
// Arrange
|
|
65
|
+
const action = jest.fn();
|
|
66
|
+
const {rerender} = renderHook(
|
|
67
|
+
({active}) => useTimeout(action, 500, active),
|
|
68
|
+
{initialProps: {active: true}},
|
|
69
|
+
);
|
|
265
70
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
});
|
|
71
|
+
// Act
|
|
72
|
+
rerender({active: false});
|
|
73
|
+
jest.advanceTimersByTime(501);
|
|
270
74
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
75
|
+
// Assert
|
|
76
|
+
expect(action).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
274
78
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const {result} = renderHook(() =>
|
|
279
|
-
useTimeout(action, 1000, {
|
|
280
|
-
schedulePolicy: SchedulePolicy.OnDemand,
|
|
281
|
-
}),
|
|
282
|
-
);
|
|
79
|
+
it("should reset the timeout if 'timeoutMs' is changes", () => {
|
|
80
|
+
// Arrange
|
|
81
|
+
const action = jest.fn();
|
|
283
82
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
83
|
+
// Act
|
|
84
|
+
const {rerender} = renderHook(
|
|
85
|
+
({timeoutMs}) => useTimeout(action, timeoutMs, true),
|
|
86
|
+
{initialProps: {timeoutMs: 500}},
|
|
87
|
+
);
|
|
88
|
+
rerender({timeoutMs: 1000});
|
|
89
|
+
jest.advanceTimersByTime(501);
|
|
289
90
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
91
|
+
// Assert
|
|
92
|
+
expect(action).not.toHaveBeenCalled();
|
|
93
|
+
jest.advanceTimersByTime(1001);
|
|
94
|
+
expect(action).toHaveBeenCalled();
|
|
95
|
+
});
|
|
293
96
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
schedulePolicy: SchedulePolicy.OnDemand,
|
|
300
|
-
}),
|
|
301
|
-
);
|
|
97
|
+
it("should not reset the timeout if 'action' changes", () => {
|
|
98
|
+
// Arrange
|
|
99
|
+
const action1 = jest.fn();
|
|
100
|
+
const action2 = jest.fn();
|
|
101
|
+
const timeoutSpy = jest.spyOn(window, "setTimeout");
|
|
302
102
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
103
|
+
// Act
|
|
104
|
+
const {rerender} = renderHook(
|
|
105
|
+
({action}) => useTimeout(action, 500, true),
|
|
106
|
+
{initialProps: {action: action1}},
|
|
107
|
+
);
|
|
108
|
+
// NOTE: For some reason setTimeout is called twice by the time we get
|
|
109
|
+
// here. I've verified that it only gets called once inside the hook
|
|
110
|
+
// so something else must be calling it.
|
|
111
|
+
const callCount = timeoutSpy.mock.calls.length;
|
|
112
|
+
rerender({action: action2});
|
|
113
|
+
jest.advanceTimersByTime(501);
|
|
310
114
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
115
|
+
// Assert
|
|
116
|
+
expect(timeoutSpy).toHaveBeenCalledTimes(callCount);
|
|
117
|
+
});
|
|
314
118
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
useTimeout(action, 1000, {
|
|
320
|
-
schedulePolicy: SchedulePolicy.OnDemand,
|
|
321
|
-
}),
|
|
322
|
-
);
|
|
119
|
+
it("should fire the current action if 'action' changes", () => {
|
|
120
|
+
// Arrange
|
|
121
|
+
const action1 = jest.fn();
|
|
122
|
+
const action2 = jest.fn();
|
|
323
123
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
124
|
+
// Act
|
|
125
|
+
const {rerender} = renderHook(
|
|
126
|
+
({action}) => useTimeout(action, 500, true),
|
|
127
|
+
{initialProps: {action: action1}},
|
|
128
|
+
);
|
|
129
|
+
rerender({action: action2});
|
|
130
|
+
jest.advanceTimersByTime(501);
|
|
331
131
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
132
|
+
// Assert
|
|
133
|
+
expect(action1).not.toHaveBeenCalledWith();
|
|
134
|
+
expect(action2).toHaveBeenCalledWith();
|
|
335
135
|
});
|
|
336
136
|
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {useEffect, useRef} from "react";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns a ref whose .current value is updated whenever
|
|
6
|
+
* the `value` passed to this hook changes.
|
|
7
|
+
*
|
|
8
|
+
* this is great for values that you want to reference from
|
|
9
|
+
* within a useCallback or useEffect event listener, without
|
|
10
|
+
* re-triggering the effect when the value changes
|
|
11
|
+
*
|
|
12
|
+
* @returns {{current: T}}
|
|
13
|
+
*/
|
|
14
|
+
export const useUpdatingRef = <T>(value: T): {|current: T|} => {
|
|
15
|
+
const ref = useRef<T>(value);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
ref.current = value;
|
|
18
|
+
}, [value]);
|
|
19
|
+
return ref;
|
|
20
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {useEffect} from "react";
|
|
3
|
+
|
|
4
|
+
import {useUpdatingRef} from "./internal/use-updating-ref.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A simple hook for using `setInterval`.
|
|
8
|
+
*
|
|
9
|
+
* @param action called every `intervalMs` when `active` is true
|
|
10
|
+
* @param intervalMs the duration between calls to `action`
|
|
11
|
+
* @param active whether or not the interval is active
|
|
12
|
+
*/
|
|
13
|
+
export function useInterval(
|
|
14
|
+
action: () => mixed,
|
|
15
|
+
intervalMs: number,
|
|
16
|
+
active: boolean,
|
|
17
|
+
) {
|
|
18
|
+
// We using a ref instead of a callback for `action` to avoid resetting
|
|
19
|
+
// the interval whenever the `action` changes.
|
|
20
|
+
const actionRef = useUpdatingRef(action);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (active) {
|
|
24
|
+
const intervalId = setInterval(() => {
|
|
25
|
+
actionRef.current();
|
|
26
|
+
}, intervalMs);
|
|
27
|
+
|
|
28
|
+
return () => {
|
|
29
|
+
clearInterval(intervalId);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
// actionRef isn't actually required, but react-hooks/exhaustive-deps
|
|
33
|
+
// doesn't recognize it as a ref and thus complains if it isn't in the
|
|
34
|
+
// deps list. It isn't a big deal though since the value ofactionRef
|
|
35
|
+
// never changes (only its contents do).
|
|
36
|
+
}, [intervalMs, active, actionRef]);
|
|
37
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {Meta, Story, Source, Canvas} from "@storybook/addon-docs";
|
|
2
|
+
|
|
3
|
+
import {Body, HeadingSmall} from "@khanacademy/wonder-blocks-typography";
|
|
4
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
5
|
+
import Button from "@khanacademy/wonder-blocks-button";
|
|
6
|
+
|
|
7
|
+
import {useInterval} from "./use-interval.js";
|
|
8
|
+
|
|
9
|
+
<Meta
|
|
10
|
+
title="Timing/useInterval"
|
|
11
|
+
parameters={{
|
|
12
|
+
chromatic: {
|
|
13
|
+
disableSnapshot: true,
|
|
14
|
+
},
|
|
15
|
+
}}
|
|
16
|
+
/>
|
|
17
|
+
|
|
18
|
+
# `useInterval`
|
|
19
|
+
|
|
20
|
+
`useInterval` is a hook that provides a simple API for using intervals safely.
|
|
21
|
+
It is defined as follows:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
function useInterval(
|
|
25
|
+
action: () => mixed,
|
|
26
|
+
intervalMs: number,
|
|
27
|
+
active: boolean,
|
|
28
|
+
): void;
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Notes:
|
|
32
|
+
|
|
33
|
+
- Setting `active` to `true` will start the interval and setting it to `false`
|
|
34
|
+
will stop it
|
|
35
|
+
- Changing the value of `timeoutMs` will reset the interval, changing `action`
|
|
36
|
+
will not.
|
|
37
|
+
|
|
38
|
+
export const BasicUsage = () => {
|
|
39
|
+
const [callCount, setCallCount] = React.useState(0);
|
|
40
|
+
const [active, setActive] = React.useState(false);
|
|
41
|
+
const callback = React.useCallback(() => {
|
|
42
|
+
setCallCount((callCount) => callCount + 1);
|
|
43
|
+
}, []);
|
|
44
|
+
useInterval(callback, 1000, active);
|
|
45
|
+
return (
|
|
46
|
+
<View>
|
|
47
|
+
<View>callCount = {callCount}</View>
|
|
48
|
+
<View>active = {active.toString()}</View>
|
|
49
|
+
<Button onClick={() => setActive(!active)} style={{width: 200}}>
|
|
50
|
+
Toggle active
|
|
51
|
+
</Button>
|
|
52
|
+
</View>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
<Canvas>
|
|
57
|
+
<Story name="BasicUsage">
|
|
58
|
+
<BasicUsage />
|
|
59
|
+
</Story>
|
|
60
|
+
</Canvas>
|
|
61
|
+
|
|
62
|
+
```jsx
|
|
63
|
+
export const BasicUsage = () => {
|
|
64
|
+
const [callCount, setCallCount] = React.useState(0);
|
|
65
|
+
const [active, setActive] = React.useState(false);
|
|
66
|
+
const callback = React.useCallback(() => {
|
|
67
|
+
setCallCount((callCount) => callCount + 1);
|
|
68
|
+
}, []);
|
|
69
|
+
useInterval(callback, 1000, active);
|
|
70
|
+
return (
|
|
71
|
+
<View>
|
|
72
|
+
<View>callCount = {callCount}</View>
|
|
73
|
+
<View>active = {active.toString()}</View>
|
|
74
|
+
<Button onClick={() => setActive(!active)} style={{width: 200}}>
|
|
75
|
+
Toggle active
|
|
76
|
+
</Button>
|
|
77
|
+
</View>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
```
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import {useEffect, useState, useCallback} from "react";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
SchedulePolicy as SchedulePolicies,
|
|
6
|
+
ClearPolicy as ClearPolicies,
|
|
7
|
+
} from "../util/policies.js";
|
|
8
|
+
import type {IInterval, ClearPolicy, Options} from "../util/types.js";
|
|
9
|
+
|
|
10
|
+
import {useUpdatingRef} from "./internal/use-updating-ref.js";
|
|
11
|
+
import {useInterval} from "./use-interval.js";
|
|
12
|
+
|
|
13
|
+
export function useScheduledInterval(
|
|
14
|
+
action: () => mixed,
|
|
15
|
+
intervalMs: number,
|
|
16
|
+
options?: Options,
|
|
17
|
+
): IInterval {
|
|
18
|
+
if (typeof action !== "function") {
|
|
19
|
+
throw new Error("Action must be a function");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (intervalMs < 1) {
|
|
23
|
+
throw new Error("Interval period must be >= 1");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const schedulePolicy =
|
|
27
|
+
options?.schedulePolicy ?? SchedulePolicies.Immediately;
|
|
28
|
+
|
|
29
|
+
const [isSet, setIsSet] = useState(
|
|
30
|
+
schedulePolicy === SchedulePolicies.Immediately,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const set = useCallback(() => setIsSet(true), []);
|
|
34
|
+
|
|
35
|
+
const actionRef = useUpdatingRef(action);
|
|
36
|
+
|
|
37
|
+
const clear = useCallback(
|
|
38
|
+
(policy?: ClearPolicy) => {
|
|
39
|
+
policy = policy ?? options?.clearPolicy;
|
|
40
|
+
if (isSet && policy === ClearPolicies.Resolve) {
|
|
41
|
+
actionRef.current();
|
|
42
|
+
}
|
|
43
|
+
setIsSet(false);
|
|
44
|
+
},
|
|
45
|
+
// react-hooks/exhaustive-deps doesn't require refs to be
|
|
46
|
+
// listed in the deps array. Unfortunately, in this situation
|
|
47
|
+
// it doesn't recognized actionRef as a ref.
|
|
48
|
+
[actionRef, isSet, options?.clearPolicy],
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const runOnUnmountRef = useUpdatingRef(
|
|
52
|
+
isSet && options?.clearPolicy === ClearPolicies.Resolve,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
return () => {
|
|
57
|
+
// This code will only run with the component using this
|
|
58
|
+
// hook is unmounted.
|
|
59
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
60
|
+
if (runOnUnmountRef.current) {
|
|
61
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
62
|
+
actionRef.current();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
// This eslint rule doesn't realize actionRef and runOnUnmountRef
|
|
66
|
+
// a both refs and thus do not have to be listed as deps.
|
|
67
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
useInterval(action, intervalMs, isSet);
|
|
71
|
+
|
|
72
|
+
return {isSet, set, clear};
|
|
73
|
+
}
|