@khanacademy/wonder-blocks-core 4.5.0 → 4.6.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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # @khanacademy/wonder-blocks-core
2
2
 
3
+ ## 4.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b561425a: Add useIsMounted() hook
8
+ - a566e232: Add useOnMountEffect hook
9
+ - d2b21a6e: Export useOnMountEffect hook
10
+
3
11
  ## 4.5.0
4
12
 
5
13
  ### Minor Changes
package/dist/es/index.js CHANGED
@@ -442,6 +442,21 @@ const useForceUpdate = () => {
442
442
  return forceUpdate;
443
443
  };
444
444
 
445
+ const useOnMountEffect = callback => {
446
+ React.useEffect(callback, []);
447
+ };
448
+
449
+ const useIsMounted = () => {
450
+ const isMounted = React.useRef(false);
451
+ useOnMountEffect(() => {
452
+ isMounted.current = true;
453
+ return () => {
454
+ isMounted.current = false;
455
+ };
456
+ });
457
+ return React.useCallback(() => isMounted.current, []);
458
+ };
459
+
445
460
  const useOnline = () => {
446
461
  const forceUpdate = useForceUpdate();
447
462
  useEffect$1(() => {
@@ -490,4 +505,4 @@ RenderStateRoot.defaultProps = {
490
505
  throwIfNested: true
491
506
  };
492
507
 
493
- export { IDProvider, RenderState, RenderStateRoot, server as Server, Text, UniqueIDProvider, View, WithSSRPlaceholder, addStyle, useForceUpdate, useOnline, useRenderState, useUniqueIdWithMock, useUniqueIdWithoutMock };
508
+ export { IDProvider, RenderState, RenderStateRoot, server as Server, Text, UniqueIDProvider, View, WithSSRPlaceholder, addStyle, useForceUpdate, useIsMounted, useOnMountEffect, useOnline, useRenderState, useUniqueIdWithMock, useUniqueIdWithoutMock };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanacademy/wonder-blocks-core",
3
- "version": "4.5.0",
3
+ "version": "4.6.0",
4
4
  "design": "v1",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -25,7 +25,7 @@
25
25
  "react-router-dom": "5.3.0"
26
26
  },
27
27
  "devDependencies": {
28
- "wb-dev-build-settings": "^0.4.0"
28
+ "wb-dev-build-settings": "^0.5.0"
29
29
  },
30
30
  "author": "",
31
31
  "license": "MIT"
@@ -0,0 +1,19 @@
1
+ import {Meta} from "@storybook/addon-docs";
2
+
3
+ <Meta
4
+ title="Core / Exports / useIsMounted()"
5
+ parameters={{
6
+ chromatic: {
7
+ disableSnapshot: true,
8
+ },
9
+ }}
10
+ />
11
+
12
+ # useIsMounted()
13
+
14
+ ```ts
15
+ function useIsMounted(): () => boolean;
16
+ ```
17
+
18
+ The `useIsMounted` hook returns a function that can be called to determine if
19
+ the component is mounted or not.
@@ -0,0 +1,50 @@
1
+ import {Meta} from "@storybook/addon-docs";
2
+
3
+ <Meta
4
+ title="Core / Exports / useOnMountEffect()"
5
+ parameters={{
6
+ chromatic: {
7
+ disableSnapshot: true,
8
+ },
9
+ }}
10
+ />
11
+
12
+ # useOnMountEffect()
13
+
14
+ ```ts
15
+ function useOnMountEffect(callback: (void | () => void)) void;
16
+ ```
17
+
18
+ The `useOnMountEffect` can be used to run an effect once on mount. This avoids
19
+ having to pass `useEffect` an empty deps array and disable the
20
+ `react-hooks/exhaustive-deps` lint.
21
+
22
+ If `callback` returns a cleanup function, it will be called when the component
23
+ is unmounted.
24
+
25
+ NOTE: This hook is equivalent to:
26
+
27
+ ```js
28
+ useEffect(() => {
29
+ callback();
30
+ // eslint-disable-next-line react-hooks/exhaustive-deps
31
+ }, []);
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ```js
37
+ import * as React.from "react";
38
+ import {useOnMountEffect} from "@khanacademy/wonder-blocks-core";
39
+
40
+ import {useMarkConversion} from "~/path/to/use-mark-conversion.js";
41
+
42
+ const MyComponent = (props: {}): React.Node => {
43
+ const markConversion = useMarkConversion();
44
+ useOnMountEffect(() => {
45
+ markConversion("my-conversion"); // Will only be called once, on mount
46
+ });
47
+
48
+ return <h1>Hello, world</h1>;
49
+ };
50
+ ```
@@ -1,7 +1,6 @@
1
1
  // @flow
2
2
  import * as React from "react";
3
- import {mount, shallow} from "enzyme";
4
- import "jest-enzyme";
3
+ import {render} from "@testing-library/react";
5
4
 
6
5
  import IDProvider from "../id-provider.js";
7
6
 
@@ -13,6 +12,7 @@ jest.mock("@khanacademy/wonder-blocks-core", () => {
13
12
  return {
14
13
  ...Core,
15
14
  UniqueIDProvider: (props) =>
15
+ // eslint-disable-next-line testing-library/no-node-access
16
16
  props.children({
17
17
  get: () => mockIDENTIFIER,
18
18
  }),
@@ -26,7 +26,7 @@ describe("UniqueDialog", () => {
26
26
  const titleId = "custom-title";
27
27
 
28
28
  // Act
29
- shallow(
29
+ render(
30
30
  <IDProvider id={titleId} scope="component">
31
31
  {renderDialogFn}
32
32
  </IDProvider>,
@@ -41,7 +41,7 @@ describe("UniqueDialog", () => {
41
41
  const renderDialogFn = jest.fn(() => <div />);
42
42
 
43
43
  // Act
44
- mount(<IDProvider scope="component">{renderDialogFn}</IDProvider>);
44
+ render(<IDProvider scope="component">{renderDialogFn}</IDProvider>);
45
45
 
46
46
  // Assert
47
47
  expect(renderDialogFn).toHaveBeenCalledWith(mockIDENTIFIER);
@@ -1,8 +1,7 @@
1
1
  // @flow
2
2
  import * as React from "react";
3
3
  import * as ReactDOMServer from "react-dom/server.js";
4
- import {mount} from "enzyme";
5
- import "jest-enzyme";
4
+ import {render} from "@testing-library/react";
6
5
 
7
6
  import View from "../view.js";
8
7
 
@@ -40,7 +39,7 @@ describe("UniqueIDProvider", () => {
40
39
  );
41
40
 
42
41
  // Act
43
- mount(nodes);
42
+ render(nodes);
44
43
 
45
44
  // Assert
46
45
  // Called once; for real render.
@@ -54,15 +53,15 @@ describe("UniqueIDProvider", () => {
54
53
  test("all renders get same unique id factory", () => {
55
54
  // Arrange
56
55
  const children = jest.fn(() => <View />);
57
- const nodes = (
56
+ const UnderTest = () => (
58
57
  <UniqueIDProvider mockOnFirstRender={false}>
59
58
  {children}
60
59
  </UniqueIDProvider>
61
60
  );
62
- const wrapper = mount(nodes);
61
+ const {rerender} = render(<UnderTest />);
63
62
 
64
63
  // Act
65
- wrapper.instance().forceUpdate();
64
+ rerender(<UnderTest />);
66
65
 
67
66
  // Assert
68
67
  // Check our forced render worked and we rendered three times.
@@ -103,7 +102,7 @@ describe("UniqueIDProvider", () => {
103
102
  );
104
103
 
105
104
  // Act
106
- mount(nodes);
105
+ render(nodes);
107
106
 
108
107
  // Assert
109
108
  // Called twice; once for initial mock render, and again for real
@@ -115,22 +114,22 @@ describe("UniqueIDProvider", () => {
115
114
  test("children calls after first get same unique id factory", () => {
116
115
  // Arrange
117
116
  const children = jest.fn(() => null);
118
- const nodes = (
117
+ const UnderTest = () => (
119
118
  <UniqueIDProvider mockOnFirstRender={true}>
120
119
  {children}
121
120
  </UniqueIDProvider>
122
121
  );
123
- const wrapper = mount(nodes);
122
+ const {rerender} = render(<UnderTest />);
124
123
 
125
124
  // Act
126
- wrapper.instance().forceUpdate();
125
+ rerender(<UnderTest />);
127
126
 
128
127
  // Assert
129
128
  // Check our forced render worked and we rendered three times.
130
129
  expect(children).toHaveBeenCalledTimes(3);
131
130
  // Check the second render gets a UniqueIDFactory instance.
132
131
  expect(children.mock.calls[1][0]).toBeInstanceOf(UniqueIDFactory);
133
- // Check the third render gets the same UniqueIDFactory instance.
132
+ // // Check the third render gets the same UniqueIDFactory instance.
134
133
  expect(children.mock.calls[2][0]).toBe(children.mock.calls[1][0]);
135
134
  });
136
135
  });
@@ -150,7 +149,7 @@ describe("UniqueIDProvider", () => {
150
149
  );
151
150
 
152
151
  // Act
153
- mount(nodes);
152
+ render(nodes);
154
153
 
155
154
  // Assert
156
155
  expect(foo).toHaveBeenCalled();
@@ -171,7 +170,7 @@ describe("UniqueIDProvider", () => {
171
170
  );
172
171
 
173
172
  // Act
174
- mount(nodes);
173
+ render(nodes);
175
174
 
176
175
  // Assert
177
176
  expect(foo).toHaveBeenCalled();
@@ -1,8 +1,7 @@
1
1
  // @flow
2
2
  import * as React from "react";
3
3
  import * as ReactDOMServer from "react-dom/server.js";
4
- import {mount} from "enzyme";
5
- import "jest-enzyme";
4
+ import {render} from "@testing-library/react";
6
5
 
7
6
  import WithSSRPlaceholder from "../with-ssr-placeholder.js";
8
7
  import {RenderStateRoot} from "../render-state-root.js";
@@ -23,7 +22,7 @@ describe("WithSSRPlaceholder", () => {
23
22
  );
24
23
 
25
24
  // Act
26
- mount(nodes);
25
+ render(nodes);
27
26
  });
28
27
 
29
28
  // Assert
@@ -57,7 +56,7 @@ describe("WithSSRPlaceholder", () => {
57
56
  );
58
57
 
59
58
  // Act
60
- mount(nodes);
59
+ render(nodes);
61
60
  });
62
61
 
63
62
  // Assert
@@ -90,7 +89,7 @@ describe("WithSSRPlaceholder", () => {
90
89
  );
91
90
 
92
91
  // Act
93
- mount(nodes);
92
+ render(nodes);
94
93
  });
95
94
 
96
95
  // Assert
@@ -206,7 +205,7 @@ describe("WithSSRPlaceholder", () => {
206
205
  );
207
206
 
208
207
  // Act
209
- mount(nodes);
208
+ render(nodes);
210
209
  });
211
210
 
212
211
  // Assert
@@ -0,0 +1,38 @@
1
+ // @flow
2
+ import {renderHook} from "@testing-library/react-hooks";
3
+
4
+ import {useIsMounted} from "../use-is-mounted.js";
5
+
6
+ describe("useIsMounted", () => {
7
+ it("should return false on first call", () => {
8
+ // Arrange
9
+
10
+ // Act
11
+ const {result} = renderHook(useIsMounted);
12
+
13
+ // Assert
14
+ expect(result.current()).toBeTrue();
15
+ });
16
+
17
+ it("should return true on true on subsequent calls", () => {
18
+ // Arrange
19
+ const {result, rerender} = renderHook(useIsMounted);
20
+
21
+ // Act
22
+ rerender();
23
+
24
+ // assert
25
+ expect(result.current()).toBeTrue();
26
+ });
27
+
28
+ it("should return false on unmount", () => {
29
+ // Arrange
30
+ const {result, unmount} = renderHook(useIsMounted);
31
+
32
+ // Act
33
+ unmount();
34
+
35
+ // Assert
36
+ expect(result.current()).toBeFalse();
37
+ });
38
+ });
@@ -0,0 +1,31 @@
1
+ // @flow
2
+ import {renderHook} from "@testing-library/react-hooks";
3
+
4
+ import {useOnMountEffect} from "../use-on-mount-effect.js";
5
+
6
+ describe("#useOnMountEffect", () => {
7
+ it("should call the callback once", () => {
8
+ // Arrange
9
+ const callback = jest.fn();
10
+
11
+ // Act
12
+ const {rerender} = renderHook(() => useOnMountEffect(callback));
13
+ rerender();
14
+
15
+ // Assert
16
+ expect(callback).toHaveBeenCalledTimes(1);
17
+ });
18
+
19
+ it("should call the cleanup function if one is returned by the callback", () => {
20
+ // Arrange
21
+ const cleanup = jest.fn();
22
+ const callback = jest.fn().mockReturnValue(cleanup);
23
+
24
+ // Act
25
+ const {unmount} = renderHook(() => useOnMountEffect(callback));
26
+ unmount();
27
+
28
+ // Assert
29
+ expect(cleanup).toHaveBeenCalled();
30
+ });
31
+ });
@@ -0,0 +1,23 @@
1
+ // @flow
2
+ import * as React from "react";
3
+
4
+ import {useOnMountEffect} from "./use-on-mount-effect.js";
5
+
6
+ /**
7
+ * Hook to provide a function for determining component mounted state.
8
+ *
9
+ * NOTE: Based on https://github.com/juliencrn/usehooks-ts/blob/d5f3de88cc319c790f2a4e90ba6a8904298957a5/src/useIsMounted/useIsMounted.ts
10
+ *
11
+ * @returns {() => boolean} A function that returns the component mounted state.
12
+ */
13
+ export const useIsMounted = (): (() => boolean) => {
14
+ const isMounted = React.useRef<boolean>(false);
15
+ useOnMountEffect(() => {
16
+ isMounted.current = true;
17
+ return () => {
18
+ isMounted.current = false;
19
+ };
20
+ });
21
+
22
+ return React.useCallback(() => isMounted.current, []);
23
+ };
@@ -0,0 +1,27 @@
1
+ // @flow
2
+ import * as React from "react";
3
+
4
+ /**
5
+ * Runs a callback once on mount.
6
+ *
7
+ * If the callback returns a cleanup function, it will be called when the component is unmounted.
8
+ *
9
+ * @param {() => (void | (() => void))} callback function that forces the component to update.
10
+ *
11
+ * The following code snippets are equivalent
12
+ * ```
13
+ * useOnMountEffect(() => {
14
+ * doTheThing();
15
+ * });
16
+ * ```
17
+ *
18
+ * ```
19
+ * useEffect(() => {
20
+ * doTheThing();
21
+ * // eslint-disable-next-line react-hooks/exhaustive-deps
22
+ * }, []);
23
+ */
24
+ export const useOnMountEffect = (callback: () => void | (() => void)): void => {
25
+ // eslint-disable-next-line react-hooks/exhaustive-deps
26
+ React.useEffect(callback, []);
27
+ };
package/src/index.js CHANGED
@@ -13,6 +13,8 @@ export {
13
13
  useUniqueIdWithoutMock,
14
14
  } from "./hooks/use-unique-id.js";
15
15
  export {useForceUpdate} from "./hooks/use-force-update.js";
16
+ export {useIsMounted} from "./hooks/use-is-mounted.js";
17
+ export {useOnMountEffect} from "./hooks/use-on-mount-effect.js";
16
18
  export {useOnline} from "./hooks/use-online.js";
17
19
  export {useRenderState} from "./hooks/use-render-state.js";
18
20
  export {RenderStateRoot} from "./components/render-state-root.js";
@@ -1,8 +1,7 @@
1
1
  // @flow
2
2
  import * as React from "react";
3
3
  import {StyleSheet} from "aphrodite";
4
- import {mount} from "enzyme";
5
- import "jest-enzyme";
4
+ import {screen, render} from "@testing-library/react";
6
5
 
7
6
  import addStyle from "../add-style.js";
8
7
 
@@ -27,40 +26,62 @@ describe("addStyle", () => {
27
26
  });
28
27
 
29
28
  it("should set the className if no style is provided", () => {
30
- const wrapper = mount(<StyledDiv className="foo" />);
29
+ // Arrange
30
+ render(<StyledDiv className="foo" data-test-id="styled-div" />);
31
31
 
32
- const div = wrapper.find("div");
32
+ // Act
33
+ const div = screen.getByTestId("styled-div");
33
34
 
34
- expect(div).toHaveProp("className", "foo");
35
+ // Assert
36
+ expect(div).toHaveAttribute("class", "foo");
35
37
  });
36
38
 
37
39
  it("should set the className to include foo and inlineStyles", () => {
38
- const wrapper = mount(
39
- <StyledDiv className="foo" style={{width: "100%"}} />,
40
+ // Arrange
41
+ render(
42
+ <StyledDiv
43
+ className="foo"
44
+ style={{width: "100%"}}
45
+ data-test-id="styled-div"
46
+ />,
40
47
  );
41
48
 
42
- const div = wrapper.find("div");
49
+ // Act
50
+ const div = screen.getByTestId("styled-div");
43
51
 
44
- const classNames = div.props().className.split(" ");
52
+ // Assert
53
+ const classNames = div.className.split(" ");
45
54
  expect(classNames).toHaveLength(2);
46
55
  expect(classNames[0].startsWith("inlineStyles")).toBeTruthy();
47
56
  expect(classNames[1]).toEqual("foo");
48
57
  });
49
58
 
50
- it("should set the className if an stylesheet style is provided", () => {
51
- const wrapper = mount(<StyledDiv style={styles.foo} />);
59
+ it("should set the class if an stylesheet style is provided", () => {
60
+ // Arrange
61
+ render(<StyledDiv style={styles.foo} data-test-id="styled-div" />);
52
62
 
53
- const div = wrapper.find("div");
63
+ // Act
64
+ const div = screen.getByTestId("styled-div");
54
65
 
55
- expect(div).toHaveProp("className", expect.any(String));
66
+ // Assert
67
+ expect(div).toHaveAttribute("class", expect.any(String));
56
68
  });
57
69
 
58
70
  it("should set the className if an stylesheet style is provided", () => {
59
- const wrapper = mount(<StyledDiv className="foo" style={styles.foo} />);
71
+ // Arrange
72
+ render(
73
+ <StyledDiv
74
+ className="foo"
75
+ style={styles.foo}
76
+ data-test-id="styled-div"
77
+ />,
78
+ );
60
79
 
61
- const div = wrapper.find("div");
80
+ // Act
81
+ const div = screen.getByTestId("styled-div");
62
82
 
63
- const classNames = div.props().className.split(" ");
83
+ // Assert
84
+ const classNames = div.className.split(" ");
64
85
  expect(classNames).toHaveLength(2);
65
86
  expect(classNames[0]).toEqual(expect.any(String));
66
87
  expect(classNames[1]).toEqual("foo");