@khanacademy/wonder-blocks-timing 2.0.3 → 2.1.1

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 (41) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/es/index.js +107 -230
  3. package/dist/index.js +260 -547
  4. package/dist/index.js.flow +1 -1
  5. package/package.json +3 -4
  6. package/src/__docs__/_overview_.stories.mdx +17 -0
  7. package/src/components/__docs__/migration.stories.mdx +112 -0
  8. package/{docs.md → src/components/__docs__/types.ischedule-actions.stories.mdx} +11 -260
  9. package/src/components/__docs__/with-action-scheduler-examples.js +80 -0
  10. package/src/components/__docs__/with-action-scheduler.stories.mdx +218 -0
  11. package/src/components/__tests__/action-scheduler-provider.test.js +7 -7
  12. package/src/components/__tests__/with-action-scheduler.test.js +6 -6
  13. package/src/components/action-scheduler-provider.js +2 -2
  14. package/src/components/with-action-scheduler.js +2 -2
  15. package/src/hooks/__docs__/use-interval.stories.mdx +80 -0
  16. package/src/hooks/__docs__/use-scheduled-interval.stories.mdx +147 -0
  17. package/src/hooks/{use-timeout.stories.mdx → __docs__/use-scheduled-timeout.stories.mdx} +13 -17
  18. package/src/hooks/__docs__/use-timeout.stories.mdx +80 -0
  19. package/src/hooks/__tests__/use-interval.test.js +124 -0
  20. package/src/hooks/__tests__/use-scheduled-interval.test.js +461 -0
  21. package/src/hooks/__tests__/use-scheduled-timeout.test.js +479 -0
  22. package/src/hooks/__tests__/use-timeout.test.js +94 -294
  23. package/src/hooks/internal/use-updating-ref.js +20 -0
  24. package/src/hooks/use-interval.js +37 -0
  25. package/src/hooks/use-scheduled-interval.js +73 -0
  26. package/src/hooks/use-scheduled-timeout.js +80 -0
  27. package/src/hooks/use-timeout.js +22 -55
  28. package/src/index.js +7 -4
  29. package/src/util/__tests__/action-scheduler.test.js +9 -9
  30. package/src/util/__tests__/animation-frame.test.js +2 -2
  31. package/src/util/__tests__/interval.test.js +2 -2
  32. package/src/util/__tests__/timeout.test.js +2 -2
  33. package/src/util/action-scheduler.js +4 -4
  34. package/src/util/animation-frame.js +2 -2
  35. package/src/util/interval.js +2 -2
  36. package/src/util/timeout.js +2 -2
  37. package/src/util/types.flowtest.js +2 -2
  38. package/LICENSE +0 -21
  39. package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +0 -353
  40. package/src/__tests__/generated-snapshot.test.js +0 -156
  41. package/src/components/with-action-scheduler.md +0 -18
@@ -1,2 +1,2 @@
1
1
  // @flow
2
- export * from "../src/index.js";
2
+ export * from "../src/index";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@khanacademy/wonder-blocks-timing",
3
3
  "private": false,
4
- "version": "2.0.3",
4
+ "version": "2.1.1",
5
5
  "design": "v1",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -17,9 +17,8 @@
17
17
  "react": "16.14.0"
18
18
  },
19
19
  "devDependencies": {
20
- "wb-dev-build-settings": "^0.2.0"
20
+ "wb-dev-build-settings": "^0.7.1"
21
21
  },
22
22
  "author": "",
23
- "license": "MIT",
24
- "gitHead": "9ebea88533e702011165072f090a377e02fa3f0f"
23
+ "license": "MIT"
25
24
  }
@@ -0,0 +1,17 @@
1
+ import {Meta} from "@storybook/addon-docs";
2
+
3
+ <Meta
4
+ title="Timing / Overview"
5
+ parameters={{
6
+ chromatic: {
7
+ disableSnapshot: true,
8
+ },
9
+ }}
10
+ />
11
+
12
+ # Timing
13
+
14
+ `wonder-blocks-timing` provides abstractions for common timing APIs like
15
+ `setTimeout`, `setInterval` and `requestAnimationFrame` that are aware of React
16
+ component lifecycles, ensuring that scheduled timer actions are not unexpectedly
17
+ dangling after a component unmounts.
@@ -0,0 +1,112 @@
1
+ import {Meta} from "@storybook/addon-docs";
2
+
3
+ <Meta
4
+ title="Timing / withActionScheduler / Migration from standard API"
5
+ parameters={{
6
+ chromatic: {
7
+ disableSnapshot: true,
8
+ },
9
+ }}
10
+ />
11
+
12
+ # withActionSceduler
13
+
14
+ ## Migration from standard API
15
+
16
+ Migrating from the standard API can be done by:
17
+
18
+ 1. Wrapping your component with the `withActionScheduler` HOC (and, if using Flow, using the `WithActionSchedulerProps` type to extend your components props by spreading the type into
19
+ your component's `Props` type)
20
+ 2. Using the new `schedule` prop in your component instead of `setTimeout`, `setInterval` and `requestAnimationFrame`
21
+
22
+ ### Migration Example
23
+
24
+ Let's imagine we have a component that uses `setTimeout` like this:
25
+
26
+ ```js static
27
+ type Props = {||};
28
+
29
+ type State = {
30
+ timerFired: boolean,
31
+ }
32
+
33
+ class MyLegacyComponent extends React.Component<Props, State> {
34
+ _timeoutID: TimeoutID;
35
+
36
+ state: State = {
37
+ timerFired: false;
38
+ };
39
+
40
+ componentDidMount() {
41
+ this.timeoutID = setTimeout(
42
+ () => this.setState({timerFired: true}),
43
+ 2000,
44
+ );
45
+ }
46
+
47
+ componentWillUnmount() {
48
+ /* 0 is a valid ID for a timeout */
49
+ if (this.timeoutID != null) {
50
+ clearTimeout(this.timeoutID);
51
+ }
52
+ }
53
+
54
+ renderState() {
55
+ const {timerFired} = this.state;
56
+ if (timerFired) {
57
+ return "...fired!";
58
+ }
59
+ return "pending...";
60
+ }
61
+
62
+ render() {
63
+ return <View>Legacy Component {this.renderState()}</View>;
64
+ }
65
+ }
66
+ ```
67
+
68
+ We can rewrite it to use the Wonder Blocks Timing API like this:
69
+
70
+ ```js static
71
+ type Props = {|
72
+ /**
73
+ * These props will be injected into your component. They won't appear
74
+ * as part of the public props of the component since `withActionSceduler`
75
+ * will excluding them from the props of the component it returns.
76
+ */
77
+ ...withActionSchedulerProps
78
+ |};
79
+
80
+ type State = {
81
+ timerFired: boolean,
82
+ }
83
+
84
+ class MyWonderBlocksComponentImpl extends React.Component<Props, State> {
85
+ state: State = {
86
+ timerFired: false;
87
+ };
88
+
89
+ componentDidMount() {
90
+ const {schedule} = this.props;
91
+ schedule.timeout(() => this.setState({timerFired: true}), 2000);
92
+ }
93
+
94
+ renderState() {
95
+ const {timerFired} = this.state;
96
+ if (timerFired) {
97
+ return "...fired!";
98
+ }
99
+ return "pending...";
100
+ }
101
+
102
+ render() {
103
+ return <View>Wonder Blocks Component {this.renderState()}</View>;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * The component that you would export as a drop-in replacement for your
109
+ * legacy component.
110
+ */
111
+ const MyWonderBlocksComponent = withActionScheduler(MyWonderBlocksComponentImpl);
112
+ ```
@@ -1,164 +1,15 @@
1
- Abstractions for common timing APIs like `setTimeout`, `setInterval` and
2
- `requestAnimationFrame` that are aware of React component lifecycles, ensuring
3
- that scheduled timer actions are not unexpectedly dangling after a component
4
- unmounts.
5
-
6
- Access to the timing API is provided via the `withActionScheduler` higher order
7
- component.
8
-
9
- ### Timing API Usage Example
10
-
11
- The following component, `MyNaughtyComponent`, will keep spamming our pretend
12
- log even after it was unmounted.
13
-
14
- ```jsx
15
- import Button from "@khanacademy/wonder-blocks-button";
16
- import {IDProvider, View} from "@khanacademy/wonder-blocks-core";
17
-
18
- class Unmounter extends React.Component {
19
- constructor() {
20
- super();
21
- this.state = {
22
- mountKids: true,
23
- };
24
- }
25
-
26
- maybeRenderKids() {
27
- if (this.state.mountKids) {
28
- return (
29
- <React.Fragment>
30
- <Button onClick={() => this.onClick()}>Unmount</Button>
31
- {this.props.children}
32
- </React.Fragment>
33
- );
34
- } else {
35
- return "Children unmounted";
36
- }
37
- }
38
-
39
- onClick() {
40
- this.setState({mountKids: false});
41
- }
42
-
43
- render() {
44
- return (
45
- <View>
46
- {this.maybeRenderKids()}
47
- </View>
48
- );
49
- }
50
- }
51
-
52
- class MyNaughtyComponent extends React.Component {
53
- componentDidMount() {
54
- const {targetId} = this.props;
55
- let counter = 0;
56
- const domElement = document.getElementById(targetId);
57
- setInterval(() => {
58
- domElement.innerText = "Naughty interval logged: " + counter++;
59
- }, 200);
60
- }
61
-
62
- render() {
63
- return <View>NaughtyComponent here</View>;
64
- }
65
- }
66
-
67
-
68
- <IDProvider>
69
- {id => (
70
- <View>
71
- <Unmounter>
72
- <MyNaughtyComponent targetId={id} />
73
- </Unmounter>
74
- <View>
75
- <View id={id}></View>
76
- </View>
77
- </View>
78
- )}
79
- </IDProvider>
80
- ```
81
-
82
- But if we use `withActionScheduler` and the `interval` method, everything is
83
- fine. Unmount the component, and the logging stops.
84
-
85
- ```jsx
86
- import {withActionScheduler} from "@khanacademy/wonder-blocks-timing";
87
- import Button from "@khanacademy/wonder-blocks-button";
88
- import {IDProvider, View} from "@khanacademy/wonder-blocks-core";
89
-
90
- class Unmounter extends React.Component {
91
- constructor() {
92
- super();
93
- this.state = {
94
- mountKids: true,
95
- };
96
- }
97
-
98
- maybeRenderKids() {
99
- if (this.state.mountKids) {
100
- return (
101
- <React.Fragment>
102
- <Button onClick={() => this.onClick()}>Unmount</Button>
103
- {this.props.children}
104
- </React.Fragment>
105
- );
106
- } else {
107
- return "Children unmounted";
108
- }
109
- }
110
-
111
- onClick() {
112
- this.setState({mountKids: false});
113
- }
114
-
115
- render() {
116
- return (
117
- <View>
118
- {this.maybeRenderKids()}
119
- </View>
120
- );
121
- }
122
- }
1
+ import {Meta} from "@storybook/addon-docs";
123
2
 
124
- class MyGoodComponent extends React.Component {
125
- componentDidMount() {
126
- const {targetId, schedule} = this.props;
127
- let counter = 0;
128
- const domElement = document.getElementById(targetId);
129
- schedule.interval(() => {
130
- domElement.innerText = "Naughty interval logged: " + counter++;
131
- }, 200);
132
- }
133
-
134
- render() {
135
- return <View>GoodComponent here</View>;
136
- }
137
- }
138
-
139
- const MyGoodComponentWithScheduler = withActionScheduler(MyGoodComponent);
140
-
141
- <IDProvider>
142
- {id => (
143
- <View>
144
- <Unmounter>
145
- <MyGoodComponentWithScheduler targetId={id} />
146
- </Unmounter>
147
- <View>
148
- <View id={id}></View>
149
- </View>
150
- </View>
151
- )}
152
- </IDProvider>
153
- ```
154
-
155
- <!-- Styleguidist doesn't support the extended syntax for custom header IDs.
156
- If that ever changes, this should be:
157
- `### API Overiview {#timing-api-overview}`
158
- -->
159
- <h3 id="timing-api-overview">API Overview</h3>
3
+ <Meta
4
+ title="Timing / withActionScheduler / Types / IScheduleActions"
5
+ parameters={{
6
+ chromatic: {
7
+ disableSnapshot: true,
8
+ },
9
+ }}
10
+ />
160
11
 
161
- #### IScheduleActions
12
+ # IScheduleActions
162
13
 
163
14
  The `IScheduleActions` interface provides 4 (four) different functions:
164
15
 
@@ -303,104 +154,4 @@ The `IAnimationFrame` interface provides additional calls to manipulate an anima
303
154
  | --- | --- | --- |
304
155
  | `isSet` | `boolean` | A read-only property for determining if the request is set (aka pending). Returns `true` if the animation frame is set, otherwise `false`. |
305
156
  | `set` | `()`&nbsp;`=>`&nbsp;`void` | If the request is pending, this cancels that pending request and starts a request afresh. If the request is not pending, this starts the request. Can be used to re-request an already invokd or cleared request. |
306
- | `clear` | `(clearPolicy?:`&nbsp;`ClearPolicy)`&nbsp;`=>`&nbsp;`void` | If the request is pending, this cancels that pending request. If no request is pending, this does nothing. When the optional `clearPolicy` argument is `ClearPolicy.Resolve`, and the request was in the set state when called, the associated action is invoked after cancelling the requst. The `clearPolicy` parameter defaults to `ClearPolicy.Cancel`. This call does nothing if there was no pending request (i.e. when `isSet` is `false`). |
307
-
308
- ### Migration from standard API
309
-
310
- Migrating from the standard API can be done by:
311
-
312
- 1. Wrapping your component with the `withActionScheduler` HOC (and, if using Flow, using the `WithActionSchedulerProps` type to extend your components props by spreading the type into
313
- your component's `Props` type)
314
- 2. Using the new `schedule` prop in your component instead of `setTimeout`, `setInterval` and `requestAnimationFrame`
315
-
316
- #### Migration Example
317
-
318
- Let's imagine we have a component that uses `setTimeout` like this:
319
-
320
- ```js static
321
- type Props = {||};
322
-
323
- type State = {
324
- timerFired: boolean,
325
- }
326
-
327
- class MyLegacyComponent extends React.Component<Props, State> {
328
- _timeoutID: TimeoutID;
329
-
330
- state: State = {
331
- timerFired: false;
332
- };
333
-
334
- componentDidMount() {
335
- this.timeoutID = setTimeout(
336
- () => this.setState({timerFired: true}),
337
- 2000,
338
- );
339
- }
340
-
341
- componentWillUnmount() {
342
- /* 0 is a valid ID for a timeout */
343
- if (this.timeoutID != null) {
344
- clearTimeout(this.timeoutID);
345
- }
346
- }
347
-
348
- renderState() {
349
- const {timerFired} = this.state;
350
- if (timerFired) {
351
- return "...fired!";
352
- }
353
- return "pending...";
354
- }
355
-
356
- render() {
357
- return <View>Legacy Component {this.renderState()}</View>;
358
- }
359
- }
360
- ```
361
-
362
- We can rewrite it to use the Wonder Blocks Timing API like this:
363
-
364
- ```js static
365
- type Props = {|
366
- /**
367
- * These props will be injected into your component. They won't appear
368
- * as part of the public props of the component since `withActionSceduler`
369
- * will excluding them from the props of the component it returns.
370
- */
371
- ...withActionSchedulerProps
372
- |};
373
-
374
- type State = {
375
- timerFired: boolean,
376
- }
377
-
378
- class MyWonderBlocksComponentImpl extends React.Component<Props, State> {
379
- state: State = {
380
- timerFired: false;
381
- };
382
-
383
- componentDidMount() {
384
- const {schedule} = this.props;
385
- schedule.timeout(() => this.setState({timerFired: true}), 2000);
386
- }
387
-
388
- renderState() {
389
- const {timerFired} = this.state;
390
- if (timerFired) {
391
- return "...fired!";
392
- }
393
- return "pending...";
394
- }
395
-
396
- render() {
397
- return <View>Wonder Blocks Component {this.renderState()}</View>;
398
- }
399
- }
400
-
401
- /**
402
- * The component that you would export as a drop-in replacement for your
403
- * legacy component.
404
- */
405
- const MyWonderBlocksComponent = withActionScheduler(MyWonderBlocksComponentImpl);
406
- ```
157
+ | `clear` | `(clearPolicy?:`&nbsp;`ClearPolicy)`&nbsp;`=>`&nbsp;`void` | If the request is pending, this cancels that pending request. If no request is pending, this does nothing. When the optional `clearPolicy` argument is `ClearPolicy.Resolve`, and the request was in the set state when called, the associated action is invoked after cancelling the requst. The `clearPolicy` parameter defaults to `ClearPolicy.Cancel`. This call does nothing if there was no pending request (i.e. when `isSet` is `false`). |
@@ -0,0 +1,80 @@
1
+ // @flow
2
+ import * as React from "react";
3
+ import Button from "@khanacademy/wonder-blocks-button";
4
+ import {View} from "@khanacademy/wonder-blocks-core";
5
+ import {withActionScheduler} from "@khanacademy/wonder-blocks-timing";
6
+ import type {
7
+ WithActionSchedulerProps,
8
+ WithoutActionScheduler,
9
+ } from "@khanacademy/wonder-blocks-timing";
10
+
11
+ export function Unmounter(props: {|children: React.Node|}): React.Node {
12
+ const [mountKids, setMountKids] = React.useState(true);
13
+
14
+ const maybeRenderKids = () => {
15
+ if (!mountKids) {
16
+ return "Children unmounted";
17
+ }
18
+
19
+ return (
20
+ <>
21
+ <Button
22
+ onClick={() => {
23
+ setMountKids(false);
24
+ }}
25
+ >
26
+ Unmount
27
+ </Button>
28
+ {props.children}
29
+ </>
30
+ );
31
+ };
32
+
33
+ return <View>{maybeRenderKids()}</View>;
34
+ }
35
+
36
+ export class MyNaughtyComponent extends React.Component<{|targetId: string|}> {
37
+ componentDidMount() {
38
+ const {targetId} = this.props;
39
+ let counter = 0;
40
+ const domElement: HTMLElement = (document.getElementById(
41
+ targetId,
42
+ ): any);
43
+
44
+ setInterval(() => {
45
+ domElement.innerText = "Naughty interval logged: " + counter++;
46
+ }, 200);
47
+ }
48
+
49
+ render(): React.Node {
50
+ return <View>NaughtyComponent here</View>;
51
+ }
52
+ }
53
+
54
+ type Props = {|
55
+ ...WithActionSchedulerProps,
56
+ targetId: string,
57
+ |};
58
+
59
+ export class MyGoodComponent extends React.Component<Props> {
60
+ componentDidMount() {
61
+ const {targetId, schedule} = this.props;
62
+ let counter = 0;
63
+ const domElement: HTMLElement = (document.getElementById(
64
+ targetId,
65
+ ): any);
66
+
67
+ schedule.interval(() => {
68
+ domElement.innerText = "Naughty interval logged: " + counter++;
69
+ }, 200);
70
+ }
71
+
72
+ render(): React.Node {
73
+ return <View>GoodComponent here</View>;
74
+ }
75
+ }
76
+
77
+ export const MyGoodComponentWithScheduler: React.AbstractComponent<
78
+ WithoutActionScheduler<React.ElementConfig<typeof MyGoodComponent>>,
79
+ MyGoodComponent,
80
+ > = withActionScheduler(MyGoodComponent);