@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
|
@@ -0,0 +1,147 @@
|
|
|
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 {ClearPolicy, SchedulePolicy} from "../util/policies.js";
|
|
8
|
+
import {useScheduledInterval} from "./use-scheduled-interval.js";
|
|
9
|
+
|
|
10
|
+
<Meta
|
|
11
|
+
title="Timing/useScheduledInterval"
|
|
12
|
+
parameters={{
|
|
13
|
+
chromatic: {
|
|
14
|
+
disableSnapshot: true,
|
|
15
|
+
},
|
|
16
|
+
}}
|
|
17
|
+
/>
|
|
18
|
+
|
|
19
|
+
# `useScheduledInterval`
|
|
20
|
+
|
|
21
|
+
`useScheduledInterval` is a hook that provides a convenient API for setting and clearing
|
|
22
|
+
an interval. It is defined as follows:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
function useScheduledInterval(
|
|
26
|
+
action: () => mixed,
|
|
27
|
+
timeoutMs: number,
|
|
28
|
+
options?: {|
|
|
29
|
+
schedulePolicy?: "schedule-immediately" | "schedule-on-demand",
|
|
30
|
+
clearPolicy?: "resolve-on-clear" | "cancel-on-clear",
|
|
31
|
+
|},
|
|
32
|
+
): ITimeout;
|
|
33
|
+
|
|
34
|
+
interface ITimeout {
|
|
35
|
+
get isSet(): boolean;
|
|
36
|
+
set(): void;
|
|
37
|
+
clear(policy?: ClearPolicy): void;
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
By default the interval will be set immediately up creation. The `options` parameter can
|
|
42
|
+
be used to control when when the interval is schedule and whether or not `action` should be
|
|
43
|
+
called when the interval is cleared.
|
|
44
|
+
|
|
45
|
+
Notes:
|
|
46
|
+
|
|
47
|
+
- Because `clear` takes a param, it's import that you don't pass it directly to an event handler,
|
|
48
|
+
e.g. `<Button onClick={clear} />` will not work as expected.
|
|
49
|
+
- Calling `set` after the interval has been cleared will restart the interval.
|
|
50
|
+
- Updating the second paramter, `timeoutMs`, will also restart the interval.
|
|
51
|
+
- When the component using this hooks is unmounted, the interval will automatically be cleared.
|
|
52
|
+
- Calling `set` after the interval is already set does nothing.
|
|
53
|
+
|
|
54
|
+
export const Immediately = () => {
|
|
55
|
+
const [callCount, setCallCount] = React.useState(0);
|
|
56
|
+
const callback = React.useCallback(() => {
|
|
57
|
+
setCallCount((callCount) => callCount + 1);
|
|
58
|
+
}, []);
|
|
59
|
+
const interval = useScheduledInterval(callback, 1000);
|
|
60
|
+
return (
|
|
61
|
+
<View>
|
|
62
|
+
<View>isSet = {interval.isSet.toString()}</View>
|
|
63
|
+
<View>callCount = {callCount}</View>
|
|
64
|
+
<View style={{flexDirection: "row"}}>
|
|
65
|
+
<Button onClick={() => interval.set()}>Set interval</Button>
|
|
66
|
+
<Button onClick={() => interval.clear()}>Clear interval</Button>
|
|
67
|
+
</View>
|
|
68
|
+
</View>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
<Canvas>
|
|
73
|
+
<Story name="Immediately">
|
|
74
|
+
<Immediately />
|
|
75
|
+
</Story>
|
|
76
|
+
</Canvas>
|
|
77
|
+
|
|
78
|
+
```jsx
|
|
79
|
+
const Immediately = () => {
|
|
80
|
+
const [callCount, setCallCount] = React.useState(0);
|
|
81
|
+
const callback = React.useCallback(() => {
|
|
82
|
+
setCallCount((callCount) => callCount + 1);
|
|
83
|
+
}, []);
|
|
84
|
+
const interval = useScheduledInterval(callback, 1000);
|
|
85
|
+
return (
|
|
86
|
+
<View>
|
|
87
|
+
<View>isSet = {interval.isSet.toString()}</View>
|
|
88
|
+
<View>callCount = {callCount}</View>
|
|
89
|
+
<View style={{flexDirection: "row"}}>
|
|
90
|
+
<Button onClick={() => interval.set()}>Set interval</Button>
|
|
91
|
+
<Button onClick={() => interval.clear()}>Clear interval</Button>
|
|
92
|
+
</View>
|
|
93
|
+
</View>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
export const OnDemandAndResolveOnClear = () => {
|
|
99
|
+
const [callCount, setCallCount] = React.useState(0);
|
|
100
|
+
const callback = React.useCallback(() => {
|
|
101
|
+
console.log("action called");
|
|
102
|
+
setCallCount((callCount) => callCount + 1);
|
|
103
|
+
}, []);
|
|
104
|
+
const interval = useScheduledInterval(callback, 1000, {
|
|
105
|
+
clearPolicy: ClearPolicy.Resolve,
|
|
106
|
+
schedulePolicy: SchedulePolicy.OnDemand,
|
|
107
|
+
});
|
|
108
|
+
return (
|
|
109
|
+
<View>
|
|
110
|
+
<View>isSet = {interval.isSet.toString()}</View>
|
|
111
|
+
<View>callCount = {callCount}</View>
|
|
112
|
+
<View style={{flexDirection: "row"}}>
|
|
113
|
+
<Button onClick={() => interval.set()}>Set interval</Button>
|
|
114
|
+
<Button onClick={() => interval.clear()}>Clear interval</Button>
|
|
115
|
+
</View>
|
|
116
|
+
</View>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
<Canvas>
|
|
121
|
+
<Story name="OnDemandAndResolveOnClear">
|
|
122
|
+
<OnDemandAndResolveOnClear />
|
|
123
|
+
</Story>
|
|
124
|
+
</Canvas>
|
|
125
|
+
|
|
126
|
+
```jsx
|
|
127
|
+
const OnDemandAndResolveOnClear = () => {
|
|
128
|
+
const [callCount, setCallCount] = React.useState(0);
|
|
129
|
+
const callback = React.useCallback(() => {
|
|
130
|
+
setCallCount((callCount) => callCount + 1);
|
|
131
|
+
}, []);
|
|
132
|
+
const interval = useScheduledInterval(callback, 1000, {
|
|
133
|
+
clearPolicy: ClearPolicy.Resolve,
|
|
134
|
+
schedulePolicy: SchedulePolicy.OnDemand,
|
|
135
|
+
});
|
|
136
|
+
return (
|
|
137
|
+
<View>
|
|
138
|
+
<View>isSet = {interval.isSet.toString()}</View>
|
|
139
|
+
<View>callCount = {callCount}</View>
|
|
140
|
+
<View style={{flexDirection: "row"}}>
|
|
141
|
+
<Button onClick={() => interval.set()}>Set interval</Button>
|
|
142
|
+
<Button onClick={() => interval.clear()}>Clear interval</Button>
|
|
143
|
+
</View>
|
|
144
|
+
</View>
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
```
|
|
@@ -0,0 +1,80 @@
|
|
|
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 {ITimeout, ClearPolicy, Options} from "../util/types.js";
|
|
9
|
+
|
|
10
|
+
import {useUpdatingRef} from "./internal/use-updating-ref.js";
|
|
11
|
+
import {useTimeout} from "./use-timeout.js";
|
|
12
|
+
|
|
13
|
+
export function useScheduledTimeout(
|
|
14
|
+
action: () => mixed,
|
|
15
|
+
timeoutMs: number,
|
|
16
|
+
options?: Options,
|
|
17
|
+
): ITimeout {
|
|
18
|
+
if (typeof action !== "function") {
|
|
19
|
+
throw new Error("Action must be a function");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (timeoutMs < 0) {
|
|
23
|
+
throw new Error("Timeout period must be >= 0");
|
|
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
|
+
// This wrapper isn't present in useScheduledInterval because we
|
|
36
|
+
// don't need to update `isSet` in that situations.
|
|
37
|
+
const wrappedAction = useCallback(() => {
|
|
38
|
+
setIsSet(false);
|
|
39
|
+
action();
|
|
40
|
+
}, [action]);
|
|
41
|
+
|
|
42
|
+
const actionRef = useUpdatingRef(wrappedAction);
|
|
43
|
+
|
|
44
|
+
const clear = useCallback(
|
|
45
|
+
(policy?: ClearPolicy) => {
|
|
46
|
+
policy = policy ?? options?.clearPolicy;
|
|
47
|
+
if (isSet && policy === ClearPolicies.Resolve) {
|
|
48
|
+
actionRef.current();
|
|
49
|
+
}
|
|
50
|
+
setIsSet(false);
|
|
51
|
+
},
|
|
52
|
+
// react-hooks/exhaustive-deps doesn't require refs to be
|
|
53
|
+
// listed in the deps array. Unfortunately, in this situation
|
|
54
|
+
// it doesn't recognized actionRef as a ref.
|
|
55
|
+
[actionRef, isSet, options?.clearPolicy],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const runOnUnmountRef = useUpdatingRef(
|
|
59
|
+
isSet && options?.clearPolicy === ClearPolicies.Resolve,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
return () => {
|
|
64
|
+
// This code will only run with the component using this
|
|
65
|
+
// hook is unmounted.
|
|
66
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
67
|
+
if (runOnUnmountRef.current) {
|
|
68
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
69
|
+
actionRef.current();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
// This eslint rule doesn't realize actionRef and runOnUnmountRef
|
|
73
|
+
// a both refs and thus do not have to be listed as deps.
|
|
74
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
useTimeout(wrappedAction, timeoutMs, isSet);
|
|
78
|
+
|
|
79
|
+
return {isSet, set, clear};
|
|
80
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
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 {ClearPolicy, SchedulePolicy} from "../util/policies.js";
|
|
8
|
+
import {useScheduledTimeout} from "./use-scheduled-timeout.js";
|
|
9
|
+
|
|
10
|
+
<Meta
|
|
11
|
+
title="Timing/useScheduledTimeout"
|
|
12
|
+
parameters={{
|
|
13
|
+
chromatic: {
|
|
14
|
+
disableSnapshot: true,
|
|
15
|
+
},
|
|
16
|
+
}}
|
|
17
|
+
/>
|
|
18
|
+
|
|
19
|
+
# `useScheduledTimeout`
|
|
20
|
+
|
|
21
|
+
`useScheduledTimeout` is a hook that provides a convenient API for setting and clearing
|
|
22
|
+
a timeout. It is defined as follows:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
function useScheduledTimeout(
|
|
26
|
+
action: () => mixed,
|
|
27
|
+
timeoutMs: number,
|
|
28
|
+
options?: {|
|
|
29
|
+
schedulePolicy?: "schedule-immediately" | "schedule-on-demand",
|
|
30
|
+
clearPolicy?: "resolve-on-clear" | "cancel-on-clear",
|
|
31
|
+
|},
|
|
32
|
+
): ITimeout;
|
|
33
|
+
|
|
34
|
+
interface ITimeout {
|
|
35
|
+
get isSet(): boolean;
|
|
36
|
+
set(): void;
|
|
37
|
+
clear(policy?: ClearPolicy): void;
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
By default the timeout will be set immediately up creation. The `options` parameter can
|
|
42
|
+
be used to control when when the timeout is schedule and whether or not `action` should be
|
|
43
|
+
called when the timeout is cleared.
|
|
44
|
+
|
|
45
|
+
Notes:
|
|
46
|
+
|
|
47
|
+
- Because `clear` takes a param, it's import that you don't pass it directly to an event handler,
|
|
48
|
+
e.g. `<Button onClick={clear} />` will not work as expected.
|
|
49
|
+
- Calling `set` after the timeout has expired will restart the timeout.
|
|
50
|
+
- Updating the second paramter, `timeoutMs`, will also restart the timeout.
|
|
51
|
+
- When the component using this hooks is unmounted, the timeout will automatically be cleared.
|
|
52
|
+
- Calling `set` after the timeout is set but before it expires means that the timeout will be
|
|
53
|
+
reset and will call `action`, `timeoutMs` after the most recent call to `set` was made.
|
|
54
|
+
|
|
55
|
+
export const Immediately = () => {
|
|
56
|
+
const [callCount, setCallCount] = React.useState(0);
|
|
57
|
+
const callback = React.useCallback(() => {
|
|
58
|
+
setCallCount((callCount) => callCount + 1);
|
|
59
|
+
}, []);
|
|
60
|
+
const {isSet, set, clear} = useScheduledTimeout(callback, 1000);
|
|
61
|
+
return (
|
|
62
|
+
<View>
|
|
63
|
+
<View>isSet = {isSet.toString()}</View>
|
|
64
|
+
<View>callCount = {callCount}</View>
|
|
65
|
+
<View style={{flexDirection: "row"}}>
|
|
66
|
+
<Button onClick={set}>Set timeout</Button>
|
|
67
|
+
<Button onClick={clear}>Clear timeout</Button>
|
|
68
|
+
</View>
|
|
69
|
+
</View>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
<Canvas>
|
|
74
|
+
<Story name="Immediately">
|
|
75
|
+
<Immediately />
|
|
76
|
+
</Story>
|
|
77
|
+
</Canvas>
|
|
78
|
+
|
|
79
|
+
```jsx
|
|
80
|
+
const Immediately = () => {
|
|
81
|
+
const [callCount, setCallCount] = React.useState(0);
|
|
82
|
+
const callback = React.useCallback(() => {
|
|
83
|
+
setCallCount((callCount) => callCount + 1);
|
|
84
|
+
}, []);
|
|
85
|
+
const {isSet, set, clear} = useScheduledTimeout(callback, 1000);
|
|
86
|
+
return (
|
|
87
|
+
<View>
|
|
88
|
+
<View>isSet = {isSet.toString()}</View>
|
|
89
|
+
<View>callCount = {callCount}</View>
|
|
90
|
+
<View style={{flexDirection: "row"}}>
|
|
91
|
+
<Button onClick={() => set()}>Set timeout</Button>
|
|
92
|
+
<Button onClick={() => clear()}>Clear timeout</Button>
|
|
93
|
+
</View>
|
|
94
|
+
</View>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
export const OnDemandAndResolveOnClear = () => {
|
|
100
|
+
const [callCount, setCallCount] = React.useState(0);
|
|
101
|
+
const callback = React.useCallback(() => {
|
|
102
|
+
console.log("action called");
|
|
103
|
+
setCallCount((callCount) => callCount + 1);
|
|
104
|
+
}, []);
|
|
105
|
+
const {isSet, set, clear} = useScheduledTimeout(callback, 1000, {
|
|
106
|
+
clearPolicy: ClearPolicy.Resolve,
|
|
107
|
+
schedulePolicy: SchedulePolicy.OnDemand,
|
|
108
|
+
});
|
|
109
|
+
return (
|
|
110
|
+
<View>
|
|
111
|
+
<View>isSet = {isSet.toString()}</View>
|
|
112
|
+
<View>callCount = {callCount}</View>
|
|
113
|
+
<View style={{flexDirection: "row"}}>
|
|
114
|
+
<Button onClick={() => set()}>Set timeout</Button>
|
|
115
|
+
<Button onClick={() => clear()}>Clear timeout</Button>
|
|
116
|
+
</View>
|
|
117
|
+
</View>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
<Canvas>
|
|
122
|
+
<Story name="OnDemandAndResolveOnClear">
|
|
123
|
+
<OnDemandAndResolveOnClear />
|
|
124
|
+
</Story>
|
|
125
|
+
</Canvas>
|
|
126
|
+
|
|
127
|
+
```jsx
|
|
128
|
+
const OnDemandAndResolveOnClear = () => {
|
|
129
|
+
const [callCount, setCallCount] = React.useState(0);
|
|
130
|
+
const callback = React.useCallback(() => {
|
|
131
|
+
setCallCount((callCount) => callCount + 1);
|
|
132
|
+
}, []);
|
|
133
|
+
const {isSet, set, clear} = useScheduledTimeout(callback, 1000, {
|
|
134
|
+
clearPolicy: ClearPolicy.Resolve,
|
|
135
|
+
schedulePolicy: SchedulePolicy.OnDemand,
|
|
136
|
+
});
|
|
137
|
+
return (
|
|
138
|
+
<View>
|
|
139
|
+
<View>isSet = {isSet.toString()}</View>
|
|
140
|
+
<View>callCount = {callCount}</View>
|
|
141
|
+
<View style={{flexDirection: "row"}}>
|
|
142
|
+
<Button onClick={() => set()}>Set timeout</Button>
|
|
143
|
+
<Button onClick={() => clear()}>Clear timeout</Button>
|
|
144
|
+
</View>
|
|
145
|
+
</View>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
```
|
package/src/hooks/use-timeout.js
CHANGED
|
@@ -1,70 +1,37 @@
|
|
|
1
1
|
// @flow
|
|
2
|
-
import {useEffect
|
|
2
|
+
import {useEffect} from "react";
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
SchedulePolicy as SchedulePolicies,
|
|
6
|
-
ClearPolicy as ClearPolicies,
|
|
7
|
-
} from "../util/policies.js";
|
|
8
|
-
import type {ITimeout, ClearPolicy, Options} from "../util/types.js";
|
|
4
|
+
import {useUpdatingRef} from "./internal/use-updating-ref.js";
|
|
9
5
|
|
|
6
|
+
/**
|
|
7
|
+
* A simple hook for using `setTimeout`.
|
|
8
|
+
*
|
|
9
|
+
* @param action called after `timeoutMs` when `active` is true
|
|
10
|
+
* @param timeoutMs the duration after which `action` is called
|
|
11
|
+
* @param active whether or not the interval is active
|
|
12
|
+
*/
|
|
10
13
|
export function useTimeout(
|
|
11
14
|
action: () => mixed,
|
|
12
15
|
timeoutMs: number,
|
|
13
|
-
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
schedulePolicy === SchedulePolicies.Immediately,
|
|
19
|
-
);
|
|
20
|
-
const actionRef = useRef(action);
|
|
21
|
-
const mountedRef = useRef(false);
|
|
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);
|
|
22
21
|
|
|
23
22
|
useEffect(() => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
mountedRef.current = false;
|
|
27
|
-
};
|
|
28
|
-
}, []);
|
|
29
|
-
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
actionRef.current = action;
|
|
32
|
-
}, [action]);
|
|
33
|
-
|
|
34
|
-
const clear = useCallback(
|
|
35
|
-
(policy?: ClearPolicy) => {
|
|
36
|
-
if ((policy ?? options?.clearPolicy) === ClearPolicies.Resolve) {
|
|
23
|
+
if (active) {
|
|
24
|
+
const timeoutId = setTimeout(() => {
|
|
37
25
|
actionRef.current();
|
|
38
|
-
}
|
|
39
|
-
// This will cause the useEffect below to re-run
|
|
40
|
-
setIsSet(false);
|
|
41
|
-
},
|
|
42
|
-
[options?.clearPolicy],
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
const set = useCallback(() => {
|
|
46
|
-
if (isSet) {
|
|
47
|
-
clear();
|
|
48
|
-
}
|
|
49
|
-
// This will cause the useEffect below to re-run
|
|
50
|
-
setIsSet(true);
|
|
51
|
-
}, [clear, isSet]);
|
|
52
|
-
|
|
53
|
-
useEffect(() => {
|
|
54
|
-
if (isSet && mountedRef.current) {
|
|
55
|
-
const timeout = window.setTimeout(() => {
|
|
56
|
-
actionRef.current();
|
|
57
|
-
setIsSet(false);
|
|
58
26
|
}, timeoutMs);
|
|
59
27
|
|
|
60
28
|
return () => {
|
|
61
|
-
|
|
62
|
-
if (!mountedRef.current) {
|
|
63
|
-
clear();
|
|
64
|
-
}
|
|
29
|
+
clearTimeout(timeoutId);
|
|
65
30
|
};
|
|
66
31
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
+
}, [timeoutMs, active, actionRef]);
|
|
70
37
|
}
|
|
@@ -4,7 +4,6 @@ import {Body, HeadingSmall} from "@khanacademy/wonder-blocks-typography";
|
|
|
4
4
|
import {View} from "@khanacademy/wonder-blocks-core";
|
|
5
5
|
import Button from "@khanacademy/wonder-blocks-button";
|
|
6
6
|
|
|
7
|
-
import {ClearPolicy, SchedulePolicy} from "../util/policies.js";
|
|
8
7
|
import {useTimeout} from "./use-timeout.js";
|
|
9
8
|
|
|
10
9
|
<Meta
|
|
@@ -18,134 +17,63 @@ import {useTimeout} from "./use-timeout.js";
|
|
|
18
17
|
|
|
19
18
|
# `useTimeout`
|
|
20
19
|
|
|
21
|
-
`useTimeout` is a hook that provides a
|
|
22
|
-
|
|
20
|
+
`useTimeout` is a hook that provides a simple API for using timers safely.
|
|
21
|
+
It is defined as follows:
|
|
23
22
|
|
|
24
23
|
```ts
|
|
25
24
|
function useTimeout(
|
|
26
25
|
action: () => mixed,
|
|
27
26
|
timeoutMs: number,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
clearPolicy?: "resolve-on-clear" | "cancel-on-clear",
|
|
31
|
-
|},
|
|
32
|
-
): ITimeout;
|
|
33
|
-
|
|
34
|
-
interface ITimeout {
|
|
35
|
-
get isSet(): boolean;
|
|
36
|
-
set(): void;
|
|
37
|
-
clear(policy?: ClearPolicy): void;
|
|
38
|
-
}
|
|
27
|
+
active: boolean,
|
|
28
|
+
): void;
|
|
39
29
|
```
|
|
40
30
|
|
|
41
|
-
By default the timeout will be set immediately up creation. The `options` parameter can
|
|
42
|
-
be used to control when when the timeout is schedule and whether or not `action` should be
|
|
43
|
-
called when the timeout is cleared.
|
|
44
|
-
|
|
45
31
|
Notes:
|
|
46
32
|
|
|
47
|
-
-
|
|
48
|
-
|
|
49
|
-
-
|
|
50
|
-
|
|
51
|
-
- When the component using this hooks is unmounted, the timeout will automatically be cleared.
|
|
52
|
-
- Calling `set` after the timeout is set but before it expires means that the timeout will be
|
|
53
|
-
reset and will call `action`, `timeoutMs` after the most recent call to `set` was made.
|
|
54
|
-
|
|
55
|
-
export const Immediately = () => {
|
|
56
|
-
const [callCount, setCallCount] = React.useState(0);
|
|
57
|
-
const callback = React.useCallback(() => {
|
|
58
|
-
setCallCount((callCount) => callCount + 1);
|
|
59
|
-
}, []);
|
|
60
|
-
const {isSet, set, clear} = useTimeout(callback, 1000);
|
|
61
|
-
return (
|
|
62
|
-
<View>
|
|
63
|
-
<View>isSet = {isSet.toString()}</View>
|
|
64
|
-
<View>callCount = {callCount}</View>
|
|
65
|
-
<View style={{flexDirection: "row"}}>
|
|
66
|
-
<Button onClick={set}>Set timeout</Button>
|
|
67
|
-
<Button onClick={clear}>Clear timeout</Button>
|
|
68
|
-
</View>
|
|
69
|
-
</View>
|
|
70
|
-
);
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
<Canvas>
|
|
74
|
-
<Story name="Immediately">
|
|
75
|
-
<Immediately />
|
|
76
|
-
</Story>
|
|
77
|
-
</Canvas>
|
|
78
|
-
|
|
79
|
-
```jsx
|
|
80
|
-
const Immediately = () => {
|
|
81
|
-
const [callCount, setCallCount] = React.useState(0);
|
|
82
|
-
const callback = React.useCallback(() => {
|
|
83
|
-
setCallCount((callCount) => callCount + 1);
|
|
84
|
-
}, []);
|
|
85
|
-
const {isSet, set, clear} = useTimeout(callback, 1000);
|
|
86
|
-
return (
|
|
87
|
-
<View>
|
|
88
|
-
<View>isSet = {isSet.toString()}</View>
|
|
89
|
-
<View>callCount = {callCount}</View>
|
|
90
|
-
<View style={{flexDirection: "row"}}>
|
|
91
|
-
<Button onClick={() => set()}>Set timeout</Button>
|
|
92
|
-
<Button onClick={() => clear()}>Clear timeout</Button>
|
|
93
|
-
</View>
|
|
94
|
-
</View>
|
|
95
|
-
);
|
|
96
|
-
};
|
|
97
|
-
```
|
|
33
|
+
- Setting `active` to `true` will start the timeout and setting it to `false`
|
|
34
|
+
will stop it
|
|
35
|
+
- Changing the value of `timeoutMs` will reset the timeout, changing `action`
|
|
36
|
+
will not.
|
|
98
37
|
|
|
99
|
-
export const
|
|
38
|
+
export const BasicUsage = () => {
|
|
100
39
|
const [callCount, setCallCount] = React.useState(0);
|
|
40
|
+
const [active, setActive] = React.useState(false);
|
|
101
41
|
const callback = React.useCallback(() => {
|
|
102
|
-
console.log("action called");
|
|
103
42
|
setCallCount((callCount) => callCount + 1);
|
|
104
43
|
}, []);
|
|
105
|
-
|
|
106
|
-
clearPolicy: ClearPolicy.Resolve,
|
|
107
|
-
schedulePolicy: SchedulePolicy.OnDemand,
|
|
108
|
-
});
|
|
44
|
+
useTimeout(callback, 1000, active);
|
|
109
45
|
return (
|
|
110
46
|
<View>
|
|
111
|
-
<View>isSet = {isSet.toString()}</View>
|
|
112
47
|
<View>callCount = {callCount}</View>
|
|
113
|
-
<View
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
</
|
|
48
|
+
<View>active = {active.toString()}</View>
|
|
49
|
+
<Button onClick={() => setActive(!active)} style={{width: 200}}>
|
|
50
|
+
Toggle active
|
|
51
|
+
</Button>
|
|
117
52
|
</View>
|
|
118
53
|
);
|
|
119
54
|
};
|
|
120
55
|
|
|
121
56
|
<Canvas>
|
|
122
|
-
<Story name="
|
|
123
|
-
<
|
|
57
|
+
<Story name="BasicUsage">
|
|
58
|
+
<BasicUsage />
|
|
124
59
|
</Story>
|
|
125
60
|
</Canvas>
|
|
126
61
|
|
|
127
62
|
```jsx
|
|
128
|
-
const
|
|
63
|
+
export const BasicUsage = () => {
|
|
129
64
|
const [callCount, setCallCount] = React.useState(0);
|
|
65
|
+
const [active, setActive] = React.useState(false);
|
|
130
66
|
const callback = React.useCallback(() => {
|
|
131
67
|
setCallCount((callCount) => callCount + 1);
|
|
132
68
|
}, []);
|
|
133
|
-
|
|
134
|
-
callback,
|
|
135
|
-
1000,
|
|
136
|
-
{
|
|
137
|
-
clearPolicy: ClearPolicy.Resolve,
|
|
138
|
-
schedulePolicy: SchedulePolicy.OnDemand,
|
|
139
|
-
},
|
|
140
|
-
);
|
|
69
|
+
useTimeout(callback, 1000, active);
|
|
141
70
|
return (
|
|
142
71
|
<View>
|
|
143
|
-
<View>isSet = {isSet.toString()}</View>
|
|
144
72
|
<View>callCount = {callCount}</View>
|
|
145
|
-
<View
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
</
|
|
73
|
+
<View>active = {active.toString()}</View>
|
|
74
|
+
<Button onClick={() => setActive(!active)} style={{width: 200}}>
|
|
75
|
+
Toggle active
|
|
76
|
+
</Button>
|
|
149
77
|
</View>
|
|
150
78
|
);
|
|
151
79
|
};
|
package/src/index.js
CHANGED
|
@@ -21,4 +21,7 @@ export type {
|
|
|
21
21
|
|
|
22
22
|
export {SchedulePolicy, ClearPolicy} from "./util/policies.js";
|
|
23
23
|
export {default as withActionScheduler} from "./components/with-action-scheduler.js";
|
|
24
|
+
export {useInterval} from "./hooks/use-interval.js";
|
|
24
25
|
export {useTimeout} from "./hooks/use-timeout.js";
|
|
26
|
+
export {useScheduledInterval} from "./hooks/use-scheduled-interval.js";
|
|
27
|
+
export {useScheduledTimeout} from "./hooks/use-scheduled-timeout.js";
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2018 Khan Academy
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|