@samitouri / QOS-React-2 / commits / def56c9797

[Fresh] Capture Hook signatures lazily on first render (#15832)

* Split the signature call into two calls This adds a render-time signature call by making __signature__ curried. We need both calls. The init time tells us which type has which signature. The render time call says when's a good time to capture the lazy Hooks tree. This is necessary for supporting inline requires. I will do that in next commit. * Lazily compute Hook list on first render This ensures inline requires don't break comparisons between Hook signatures of previous and next versions by caching Hook list at the time of first render. * Refactor computing Hook signature keys Instead of a traversal during the comparison, explicitly compute full keys. This makes it easier to debug mismatches.

Dan Abramov committed Jun 6, 2019 at 10:53 UTC def56c979756ef232bd0d3dacf79390506abae5f
4 files changed +461 -39
packages/react-fresh/src/ReactFreshBabelPlugin.js
+40 -4
@@ -410,7 +410,25 @@ export default function(babel) {
410 return;
411 }
412 seenForSignature.add(node);
413 - // Don't muatte the tree above this point.
413 + // Don't mutate the tree above this point.
414 +
415 + const sigCallID = path.scope.generateUidIdentifier('_s');
416 + path.scope.parent.push({
417 + id: sigCallID,
418 + init: t.callExpression(t.identifier('__signature__'), []),
419 + });
420 +
421 + // The signature call is split in two parts. One part is called inside the function.
422 + // This is used to signal when first render happens.
423 + path
424 + .get('body')
425 + .unshiftContainer(
426 + 'body',
427 + t.expressionStatement(t.callExpression(sigCallID, [])),
428 + );
429 +
430 + // The second call is around the function itself.
431 + // This is used to associate a type with a signature.
432
433 // Unlike with __register__, this needs to work for nested
434 // declarations too. So we need to search for a path where
@@ -429,7 +447,7 @@ export default function(babel) {
447 insertAfterPath.insertAfter(
448 t.expressionStatement(
449 t.callExpression(
432 - t.identifier('__signature__'),
450 + sigCallID,
451 createArgumentsForSignature(
452 id,
453 signature,
@@ -456,6 +474,24 @@ export default function(babel) {
474 seenForSignature.add(node);
475 // Don't mutate the tree above this point.
476
477 + const sigCallID = path.scope.generateUidIdentifier('_s');
478 + path.scope.parent.push({
479 + id: sigCallID,
480 + init: t.callExpression(t.identifier('__signature__'), []),
481 + });
482 +
483 + // The signature call is split in two parts. One part is called inside the function.
484 + // This is used to signal when first render happens.
485 + path
486 + .get('body')
487 + .unshiftContainer(
488 + 'body',
489 + t.expressionStatement(t.callExpression(sigCallID, [])),
490 + );
491 +
492 + // The second call is around the function itself.
493 + // This is used to associate a type with a signature.
494 +
495 if (path.parent.type === 'VariableDeclarator') {
496 let insertAfterPath = null;
497 path.find(p => {
@@ -475,7 +511,7 @@ export default function(babel) {
511 insertAfterPath.insertAfter(
512 t.expressionStatement(
513 t.callExpression(
478 - t.identifier('__signature__'),
514 + sigCallID,
515 createArgumentsForSignature(
516 path.parent.id,
517 signature,
@@ -489,7 +525,7 @@ export default function(babel) {
525 // let Foo = hoc(() => {})
526 path.replaceWith(
527 t.callExpression(
492 - t.identifier('__signature__'),
528 + sigCallID,
529 createArgumentsForSignature(node, signature, path.scope),
530 ),
531 );
packages/react-fresh/src/ReactFreshRuntime.js
+57 -17
@@ -15,8 +15,9 @@ import type {
15 import {REACT_MEMO_TYPE, REACT_FORWARD_REF_TYPE} from 'shared/ReactSymbols';
16
17 type Signature = {|
18 - key: string,
18 + ownKey: string,
19 forceReset: boolean,
20 + fullKey: string | null, // Contains keys of nested Hooks. Computed lazily.
21 getCustomHooks: () => Array<Function>,
22 |};
23
@@ -33,6 +34,49 @@ const familiesByType: WeakMap<any, Family> = new WeakMap();
34 // It is an array of [Family, NextType] tuples.
35 let pendingUpdates: Array<[Family, any]> = [];
36
37 +function computeFullKey(signature: Signature): string {
38 + if (signature.fullKey !== null) {
39 + return signature.fullKey;
40 + }
41 +
42 + let fullKey: string = signature.ownKey;
43 + let hooks;
44 + try {
45 + hooks = signature.getCustomHooks();
46 + } catch (err) {
47 + // This can happen in an edge case, e.g. if expression like Foo.useSomething
48 + // depends on Foo which is lazily initialized during rendering.
49 + // In that case just assume we'll have to remount.
50 + signature.forceReset = true;
51 + signature.fullKey = fullKey;
52 + return fullKey;
53 + }
54 +
55 + for (let i = 0; i < hooks.length; i++) {
56 + const hook = hooks[i];
57 + if (typeof hook !== 'function') {
58 + // Something's wrong. Assume we need to remount.
59 + signature.forceReset = true;
60 + signature.fullKey = fullKey;
61 + return fullKey;
62 + }
63 + const nestedHookSignature = allSignaturesByType.get(hook);
64 + if (nestedHookSignature === undefined) {
65 + // No signature means Hook wasn't in the source code, e.g. in a library.
66 + // We'll skip it because we can assume it won't change during this session.
67 + continue;
68 + }
69 + const nestedHookKey = computeFullKey(nestedHookSignature);
70 + if (nestedHookSignature.forceReset) {
71 + signature.forceReset = true;
72 + }
73 + fullKey += '\n---\n' + nestedHookKey;
74 + }
75 +
76 + signature.fullKey = fullKey;
77 + return fullKey;
78 +}
79 +
80 function haveEqualSignatures(prevType, nextType) {
81 const prevSignature = allSignaturesByType.get(prevType);
82 const nextSignature = allSignaturesByType.get(nextType);
@@ -43,27 +87,13 @@ function haveEqualSignatures(prevType, nextType) {
87 if (prevSignature === undefined || nextSignature === undefined) {
88 return false;
89 }
46 - if (prevSignature.key !== nextSignature.key) {
90 + if (computeFullKey(prevSignature) !== computeFullKey(nextSignature)) {
91 return false;
92 }
93 if (nextSignature.forceReset) {
94 return false;
95 }
96
53 - // TODO: we might need to calculate previous signature earlier in practice,
54 - // such as during the first time a component is resolved. We'll revisit this.
55 - const prevCustomHooks = prevSignature.getCustomHooks();
56 - const nextCustomHooks = nextSignature.getCustomHooks();
57 - if (prevCustomHooks.length !== nextCustomHooks.length) {
58 - return false;
59 - }
60 -
61 - for (let i = 0; i < nextCustomHooks.length; i++) {
62 - if (!haveEqualSignatures(prevCustomHooks[i], nextCustomHooks[i])) {
63 - return false;
64 - }
65 - }
66 -
97 return true;
98 }
99
@@ -161,8 +191,18 @@ export function setSignature(
191 getCustomHooks?: () => Array<Function>,
192 ): void {
193 allSignaturesByType.set(type, {
164 - key,
194 forceReset,
195 + ownKey: key,
196 + fullKey: null,
197 getCustomHooks: getCustomHooks || (() => []),
198 });
199 }
200 +
201 +// This is lazily called during first render for a type.
202 +// It captures Hook list at that time so inline requires don't break comparisons.
203 +export function collectCustomHooksForSignature(type: any) {
204 + const signature = allSignaturesByType.get(type);
205 + if (signature !== undefined) {
206 + computeFullKey(signature);
207 + }
208 +}
packages/react-fresh/src/__tests__/ReactFreshIntegration-test.js
+326 -9
@@ -57,12 +57,14 @@ describe('ReactFreshIntegration', () => {
57 }).code;
58 const exportsObj = {};
59 // eslint-disable-next-line no-new-func
60 - new Function('React', 'exports', '__register__', '__signature__', compiled)(
61 - React,
62 - exportsObj,
63 - __register__,
64 - __signature__,
65 - );
60 + new Function(
61 + 'global',
62 + 'React',
63 + 'exports',
64 + '__register__',
65 + '__signature__',
66 + compiled,
67 + )(global, React, exportsObj, __register__, __signature__);
68 return exportsObj.default;
69 }
70
@@ -85,9 +87,25 @@ describe('ReactFreshIntegration', () => {
87 ReactFreshRuntime.register(type, id);
88 }
89
88 - function __signature__(type, key, forceReset, getCustomHooks) {
89 - ReactFreshRuntime.setSignature(type, key, forceReset, getCustomHooks);
90 - return type;
90 + function __signature__() {
91 + let call = 0;
92 + let savedType;
93 + let hasCustomHooks;
94 + return function(type, key, forceReset, getCustomHooks) {
95 + switch (call++) {
96 + case 0:
97 + savedType = type;
98 + hasCustomHooks = typeof getCustomHooks === 'function';
99 + ReactFreshRuntime.setSignature(type, key, forceReset, getCustomHooks);
100 + break;
101 + case 1:
102 + if (hasCustomHooks) {
103 + ReactFreshRuntime.collectCustomHooksForSignature(savedType);
104 + }
105 + break;
106 + }
107 + return type;
108 + };
109 }
110
111 it('reloads function declarations', () => {
@@ -833,4 +851,303 @@ describe('ReactFreshIntegration', () => {
851 expect(el.textContent).toBe('G5');
852 }
853 });
854 +
855 + describe('with inline requires', () => {
856 + beforeEach(() => {
857 + global.FakeModuleSystem = {};
858 + });
859 +
860 + afterEach(() => {
861 + delete global.FakeModuleSystem;
862 + });
863 +
864 + it('remounts component if custom hook it uses changes order on first edit', () => {
865 + // This test verifies that remounting works even if calls to custom Hooks
866 + // were transformed with an inline requires transform, like we have on RN.
867 + // Inline requires make it harder to compare previous and next signatures
868 + // because useFancyState inline require always resolves to the newest version.
869 + // We're not actually using inline requires in the test, but it has similar semantics.
870 + if (__DEV__) {
871 + render(`
872 + const FakeModuleSystem = global.FakeModuleSystem;
873 +
874 + FakeModuleSystem.useFancyState = function(initialState) {
875 + return React.useState(initialState);
876 + };
877 +
878 + const App = () => {
879 + const [x, setX] = FakeModuleSystem.useFancyState('X');
880 + const [y, setY] = FakeModuleSystem.useFancyState('Y');
881 + return <h1>A{x}{y}</h1>;
882 + };
883 +
884 + export default App;
885 + `);
886 + let el = container.firstChild;
887 + expect(el.textContent).toBe('AXY');
888 +
889 + patch(`
890 + const FakeModuleSystem = global.FakeModuleSystem;
891 +
892 + FakeModuleSystem.useFancyState = function(initialState) {
893 + React.useEffect(() => {});
894 + return React.useState(initialState);
895 + };
896 +
897 + const App = () => {
898 + const [x, setX] = FakeModuleSystem.useFancyState('X');
899 + const [y, setY] = FakeModuleSystem.useFancyState('Y');
900 + return <h1>B{x}{y}</h1>;
901 + };
902 +
903 + export default App;
904 + `);
905 + // The useFancyState Hook added an effect,
906 + // so we had to remount the component.
907 + expect(container.firstChild).not.toBe(el);
908 + el = container.firstChild;
909 + expect(el.textContent).toBe('BXY');
910 +
911 + patch(`
912 + const FakeModuleSystem = global.FakeModuleSystem;
913 +
914 + FakeModuleSystem.useFancyState = function(initialState) {
915 + React.useEffect(() => {});
916 + return React.useState(initialState);
917 + };
918 +
919 + const App = () => {
920 + const [x, setX] = FakeModuleSystem.useFancyState('X');
921 + const [y, setY] = FakeModuleSystem.useFancyState('Y');
922 + return <h1>C{x}{y}</h1>;
923 + };
924 +
925 + export default App;
926 + `);
927 + // We didn't change anything except the header text.
928 + // So we don't expect a remount.
929 + expect(container.firstChild).toBe(el);
930 + expect(el.textContent).toBe('CXY');
931 + }
932 + });
933 +
934 + it('remounts component if custom hook it uses changes order on second edit', () => {
935 + if (__DEV__) {
936 + render(`
937 + const FakeModuleSystem = global.FakeModuleSystem;
938 +
939 + FakeModuleSystem.useFancyState = function(initialState) {
940 + return React.useState(initialState);
941 + };
942 +
943 + const App = () => {
944 + const [x, setX] = FakeModuleSystem.useFancyState('X');
945 + const [y, setY] = FakeModuleSystem.useFancyState('Y');
946 + return <h1>A{x}{y}</h1>;
947 + };
948 +
949 + export default App;
950 + `);
951 + let el = container.firstChild;
952 + expect(el.textContent).toBe('AXY');
953 +
954 + patch(`
955 + const FakeModuleSystem = global.FakeModuleSystem;
956 +
957 + FakeModuleSystem.useFancyState = function(initialState) {
958 + return React.useState(initialState);
959 + };
960 +
961 + const App = () => {
962 + const [x, setX] = FakeModuleSystem.useFancyState('X');
963 + const [y, setY] = FakeModuleSystem.useFancyState('Y');
964 + return <h1>B{x}{y}</h1>;
965 + };
966 +
967 + export default App;
968 + `);
969 + expect(container.firstChild).toBe(el);
970 + expect(el.textContent).toBe('BXY');
971 +
972 + patch(`
973 + const FakeModuleSystem = global.FakeModuleSystem;
974 +
975 + FakeModuleSystem.useFancyState = function(initialState) {
976 + React.useEffect(() => {});
977 + return React.useState(initialState);
978 + };
979 +
980 + const App = () => {
981 + const [x, setX] = FakeModuleSystem.useFancyState('X');
982 + const [y, setY] = FakeModuleSystem.useFancyState('Y');
983 + return <h1>C{x}{y}</h1>;
984 + };
985 +
986 + export default App;
987 + `);
988 + // The useFancyState Hook added an effect,
989 + // so we had to remount the component.
990 + expect(container.firstChild).not.toBe(el);
991 + el = container.firstChild;
992 + expect(el.textContent).toBe('CXY');
993 +
994 + patch(`
995 + const FakeModuleSystem = global.FakeModuleSystem;
996 +
997 + FakeModuleSystem.useFancyState = function(initialState) {
998 + React.useEffect(() => {});
999 + return React.useState(initialState);
1000 + };
1001 +
1002 + const App = () => {
1003 + const [x, setX] = FakeModuleSystem.useFancyState('X');
1004 + const [y, setY] = FakeModuleSystem.useFancyState('Y');
1005 + return <h1>D{x}{y}</h1>;
1006 + };
1007 +
1008 + export default App;
1009 + `);
1010 + // We didn't change anything except the header text.
1011 + // So we don't expect a remount.
1012 + expect(container.firstChild).toBe(el);
1013 + expect(el.textContent).toBe('DXY');
1014 + }
1015 + });
1016 +
1017 + it('recovers if evaluating Hook list throws', () => {
1018 + if (__DEV__) {
1019 + render(`
1020 + let FakeModuleSystem = null;
1021 +
1022 + global.FakeModuleSystem.useFancyState = function(initialState) {
1023 + return React.useState(initialState);
1024 + };
1025 +
1026 + const App = () => {
1027 + FakeModuleSystem = global.FakeModuleSystem;
1028 + const [x, setX] = FakeModuleSystem.useFancyState('X');
1029 + const [y, setY] = FakeModuleSystem.useFancyState('Y');
1030 + return <h1>A{x}{y}</h1>;
1031 + };
1032 +
1033 + export default App;
1034 + `);
1035 + let el = container.firstChild;
1036 + expect(el.textContent).toBe('AXY');
1037 +
1038 + patch(`
1039 + let FakeModuleSystem = null;
1040 +
1041 + global.FakeModuleSystem.useFancyState = function(initialState) {
1042 + React.useEffect(() => {});
1043 + return React.useState(initialState);
1044 + };
1045 +
1046 + const App = () => {
1047 + FakeModuleSystem = global.FakeModuleSystem;
1048 + const [x, setX] = FakeModuleSystem.useFancyState('X');
1049 + const [y, setY] = FakeModuleSystem.useFancyState('Y');
1050 + return <h1>B{x}{y}</h1>;
1051 + };
1052 +
1053 + export default App;
1054 + `);
1055 + // We couldn't evaluate the Hook signatures
1056 + // so we had to remount the component.
1057 + expect(container.firstChild).not.toBe(el);
1058 + el = container.firstChild;
1059 + expect(el.textContent).toBe('BXY');
1060 + }
1061 + });
1062 +
1063 + it('remounts component if custom hook it uses changes order behind an indirection', () => {
1064 + if (__DEV__) {
1065 + render(`
1066 + const FakeModuleSystem = global.FakeModuleSystem;
1067 +
1068 + FakeModuleSystem.useFancyState = function(initialState) {
1069 + return FakeModuleSystem.useIndirection(initialState);
1070 + };
1071 +
1072 + FakeModuleSystem.useIndirection = function(initialState) {
1073 + return FakeModuleSystem.useOtherIndirection(initialState);
1074 + };
1075 +
1076 + FakeModuleSystem.useOtherIndirection = function(initialState) {
1077 + return React.useState(initialState);
1078 + };
1079 +
1080 + const App = () => {
1081 + const [x, setX] = FakeModuleSystem.useFancyState('X');
1082 + const [y, setY] = FakeModuleSystem.useFancyState('Y');
1083 + return <h1>A{x}{y}</h1>;
1084 + };
1085 +
1086 + export default App;
1087 + `);
1088 + let el = container.firstChild;
1089 + expect(el.textContent).toBe('AXY');
1090 +
1091 + patch(`
1092 + const FakeModuleSystem = global.FakeModuleSystem;
1093 +
1094 + FakeModuleSystem.useFancyState = function(initialState) {
1095 + return FakeModuleSystem.useIndirection(initialState);
1096 + };
1097 +
1098 + FakeModuleSystem.useIndirection = function(initialState) {
1099 + return FakeModuleSystem.useOtherIndirection(initialState);
1100 + };
1101 +
1102 + FakeModuleSystem.useOtherIndirection = function(initialState) {
1103 + React.useEffect(() => {});
1104 + return React.useState(initialState);
1105 + };
1106 +
1107 + const App = () => {
1108 + const [x, setX] = FakeModuleSystem.useFancyState('X');
1109 + const [y, setY] = FakeModuleSystem.useFancyState('Y');
1110 + return <h1>B{x}{y}</h1>;
1111 + };
1112 +
1113 + export default App;
1114 + `);
1115 +
1116 + // The useFancyState Hook added an effect,
1117 + // so we had to remount the component.
1118 + expect(container.firstChild).not.toBe(el);
1119 + el = container.firstChild;
1120 + expect(el.textContent).toBe('BXY');
1121 +
1122 + patch(`
1123 + const FakeModuleSystem = global.FakeModuleSystem;
1124 +
1125 + FakeModuleSystem.useFancyState = function(initialState) {
1126 + return FakeModuleSystem.useIndirection(initialState);
1127 + };
1128 +
1129 + FakeModuleSystem.useIndirection = function(initialState) {
1130 + return FakeModuleSystem.useOtherIndirection(initialState);
1131 + };
1132 +
1133 + FakeModuleSystem.useOtherIndirection = function(initialState) {
1134 + React.useEffect(() => {});
1135 + return React.useState(initialState);
1136 + };
1137 +
1138 + const App = () => {
1139 + const [x, setX] = FakeModuleSystem.useFancyState('X');
1140 + const [y, setY] = FakeModuleSystem.useFancyState('Y');
1141 + return <h1>C{x}{y}</h1>;
1142 + };
1143 +
1144 + export default App;
1145 + `);
1146 + // We didn't change anything except the header text.
1147 + // So we don't expect a remount.
1148 + expect(container.firstChild).toBe(el);
1149 + expect(el.textContent).toBe('CXY');
1150 + }
1151 + });
1152 + });
1153 });
packages/react-fresh/src/__tests__/__snapshots__/ReactFreshBabelPlugin-test.js.snap
+38 -9
@@ -1,14 +1,17 @@
1 // Jest Snapshot v1, https://goo.gl/fbAQLP
2
3 exports[`ReactFreshBabelPlugin generates signatures for function declarations calling hooks 1`] = `
4 +var _s = __signature__();
5
6 export default function App() {
7 + _s();
8 +
9 const [foo, setFoo] = useState(0);
10 React.useEffect(() => {});
11 return <h1>{foo}</h1>;
12 }
13
11 -__signature__(App, "useState{[foo, setFoo]}\\nuseEffect{}");
14 +_s(App, "useState{[foo, setFoo]}\\nuseEffect{}");
15
16 _c = App;
17
@@ -18,21 +21,31 @@ __register__(_c, "App");
21 `;
22
23 exports[`ReactFreshBabelPlugin generates signatures for function expressions calling hooks 1`] = `
24 +var _s = __signature__(),
25 + _s2 = __signature__();
26 +
27 +export const A = _c3 = React.memo(_c2 = React.forwardRef(_c = _s((props, ref) => {
28 + _s();
29
22 -export const A = _c3 = React.memo(_c2 = React.forwardRef(_c = __signature__((props, ref) => {
30 const [foo, setFoo] = useState(0);
31 React.useEffect(() => {});
32 return <h1 ref={ref}>{foo}</h1>;
33 }, "useState{[foo, setFoo]}\\nuseEffect{}")));
34
28 -export const B = _c6 = React.memo(_c5 = React.forwardRef(_c4 = __signature__(function (props, ref) {
35 +export const B = _c6 = React.memo(_c5 = React.forwardRef(_c4 = _s2(function (props, ref) {
36 + _s2();
37 +
38 const [foo, setFoo] = useState(0);
39 React.useEffect(() => {});
40 return <h1 ref={ref}>{foo}</h1>;
41 }, "useState{[foo, setFoo]}\\nuseEffect{}")));
42
43 function hoc() {
35 - return __signature__(function Inner() {
44 + var _s3 = __signature__();
45 +
46 + return _s3(function Inner() {
47 + _s3();
48 +
49 const [foo, setFoo] = useState(0);
50 React.useEffect(() => {});
51 return <h1 ref={ref}>{foo}</h1>;
@@ -57,17 +70,24 @@ __register__(_c6, "B");
70 `;
71
72 exports[`ReactFreshBabelPlugin generates valid signature for exotic ways to call Hooks 1`] = `
73 +var _s2 = __signature__();
74
75 import FancyHook from 'fancy';
76
77 export default function App() {
78 + _s2();
79 +
80 + var _s = __signature__();
81 +
82 function useFancyState() {
83 + _s();
84 +
85 const [foo, setFoo] = React.useState(0);
86 useFancyEffect();
87 return foo;
88 }
89
70 - __signature__(useFancyState, 'useState{[foo, setFoo]}\\nuseFancyEffect{}', true);
90 + _s(useFancyState, 'useState{[foo, setFoo]}\\nuseFancyEffect{}', true);
91
92 const bar = useFancyState();
93 const baz = FancyHook.useThing();
@@ -76,7 +96,7 @@ export default function App() {
96 return <h1>{bar}{baz}</h1>;
97 }
98
79 -__signature__(App, 'useFancyState{bar}\\nuseThing{baz}\\nuseState{}\\nuseThePlatform{}', true, () => [FancyHook.useThing]);
99 +_s2(App, 'useFancyState{bar}\\nuseThing{baz}\\nuseState{}\\nuseThePlatform{}', true, () => [FancyHook.useThing]);
100
101 _c = App;
102
@@ -137,27 +157,36 @@ export default function () {}
157 `;
158
159 exports[`ReactFreshBabelPlugin includes custom hooks into the signatures 1`] = `
160 +var _s = __signature__(),
161 + _s2 = __signature__(),
162 + _s3 = __signature__();
163
164 function useFancyState() {
165 + _s();
166 +
167 const [foo, setFoo] = React.useState(0);
168 useFancyEffect();
169 return foo;
170 }
171
147 -__signature__(useFancyState, "useState{[foo, setFoo]}\\nuseFancyEffect{}", false, () => [useFancyEffect]);
172 +_s(useFancyState, "useState{[foo, setFoo]}\\nuseFancyEffect{}", false, () => [useFancyEffect]);
173
174 const useFancyEffect = () => {
175 + _s2();
176 +
177 React.useEffect(() => {});
178 };
179
153 -__signature__(useFancyEffect, "useEffect{}");
180 +_s2(useFancyEffect, "useEffect{}");
181
182 export default function App() {
183 + _s3();
184 +
185 const bar = useFancyState();
186 return <h1>{bar}</h1>;
187 }
188
160 -__signature__(App, "useFancyState{bar}", false, () => [useFancyState]);
189 +_s3(App, "useFancyState{bar}", false, () => [useFancyState]);
190
191 _c = App;
192