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

Handle changes at module boundaries (#16002)

Dan Abramov committed Jun 27, 2019 at 20:53 UTC f11540926d2b28ec1f227a79ec9e95b75708c2e3
2 files changed +375 -12
packages/react-refresh/src/ReactFreshRuntime.js
+19 -10
@@ -37,19 +37,18 @@ if (!__DEV__) {
37
38 // In old environments, we'll leak previous types after every edit.
39 const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
40 -const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
40
41 // We never remove these associations.
42 // It's OK to reference families, but use WeakMap/Set for types.
43 const allFamiliesByID: Map<string, Family> = new Map();
45 -// $FlowIssue
46 -const allTypes: WeakSet<any> | Set<any> = new PossiblyWeakSet();
44 +const allFamiliesByType: // $FlowIssue
45 +WeakMap<any, Family> | Map<any, Family> = new PossiblyWeakMap();
46 const allSignaturesByType: // $FlowIssue
47 WeakMap<any, Signature> | Map<any, Signature> = new PossiblyWeakMap();
48 // This WeakMap is read by React, so we only put families
49 // that have actually been edited here. This keeps checks fast.
50 // $FlowIssue
52 -const familiesByType: // $FlowIssue
51 +const updatedFamiliesByType: // $FlowIssue
52 WeakMap<any, Family> | Map<any, Family> = new PossiblyWeakMap();
53
54 // This is cleared on every performReactRefresh() call.
@@ -145,7 +144,8 @@ function canPreserveStateBetween(prevType, nextType) {
144 }
145
146 function resolveFamily(type) {
148 - return familiesByType.get(type);
147 + // Only check updated types to keep lookups fast.
148 + return updatedFamiliesByType.get(type);
149 }
150
151 export function performReactRefresh(): RefreshUpdate | null {
@@ -163,8 +163,8 @@ export function performReactRefresh(): RefreshUpdate | null {
163 // Now that we got a real edit, we can create associations
164 // that will be read by the React reconciler.
165 const prevType = family.current;
166 - familiesByType.set(prevType, family);
167 - familiesByType.set(nextType, family);
166 + updatedFamiliesByType.set(prevType, family);
167 + updatedFamiliesByType.set(nextType, family);
168 family.current = nextType;
169
170 // Determine whether this should be a re-render or a re-mount.
@@ -266,11 +266,9 @@ export function register(type: any, id: string): void {
266 // This can happen in an edge case, e.g. if we register
267 // return value of a HOC but it returns a cached component.
268 // Ignore anything but the first registration for each type.
269 - if (allTypes.has(type)) {
269 + if (allFamiliesByType.has(type)) {
270 return;
271 }
272 - allTypes.add(type);
273 -
272 // Create family or remember to update it.
273 // None of this bookkeeping affects reconciliation
274 // until the first performReactRefresh() call above.
@@ -281,6 +279,7 @@ export function register(type: any, id: string): void {
279 } else {
280 pendingUpdates.push([family, type]);
281 }
282 + allFamiliesByType.set(type, family);
283
284 // Visit inner types because we might not have registered them.
285 if (typeof type === 'object' && type !== null) {
@@ -345,6 +344,16 @@ export function getFamilyByID(id: string): Family | void {
344 }
345 }
346
347 +export function getFamilyByType(type: any): Family | void {
348 + if (__DEV__) {
349 + return allFamiliesByType.get(type);
350 + } else {
351 + throw new Error(
352 + 'Unexpected call to React Refresh in a production environment.',
353 + );
354 + }
355 +}
356 +
357 export function findAffectedHostInstances(
358 families: Array<Family>,
359 ): Set<Instance> {
packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js
+356 -2
@@ -21,6 +21,7 @@ let freshPlugin = require('react-refresh/babel');
21
22 describe('ReactFreshIntegration', () => {
23 let container;
24 + let exportsObj;
25
26 beforeEach(() => {
27 if (__DEV__) {
@@ -32,6 +33,7 @@ describe('ReactFreshIntegration', () => {
33 act = require('react-dom/test-utils').act;
34 container = document.createElement('div');
35 document.body.appendChild(container);
36 + exportsObj = undefined;
37 }
38 });
39
@@ -63,7 +65,7 @@ describe('ReactFreshIntegration', () => {
65 compileDestructuring && 'transform-es2015-destructuring',
66 ].filter(Boolean),
67 }).code;
66 - const exportsObj = {};
68 + exportsObj = {};
69 // eslint-disable-next-line no-new-func
70 new Function(
71 'global',
@@ -73,6 +75,10 @@ describe('ReactFreshIntegration', () => {
75 '$RefreshSig$',
76 compiled,
77 )(global, React, exportsObj, $RefreshReg$, $RefreshSig$);
78 + // Module systems will register exports as a fallback.
79 + // This is useful for cases when e.g. a class is exported,
80 + // and we don't want to propagate the update beyond this module.
81 + $RefreshReg$(exportsObj.default, 'exports.default');
82 return exportsObj.default;
83 }
84
@@ -86,9 +92,37 @@ describe('ReactFreshIntegration', () => {
92 }
93
94 function patch(source) {
95 + const prevExports = exportsObj;
96 execute(source);
97 + const nextExports = exportsObj;
98 +
99 + // Check if exported families have changed.
100 + // (In a real module system we'd do this for *all* exports.)
101 + // For example, this can happen if you convert a class to a function.
102 + // Or if you wrap something in a HOC.
103 + let didExportsChange =
104 + ReactFreshRuntime.getFamilyByType(prevExports.default) !==
105 + ReactFreshRuntime.getFamilyByType(nextExports.default);
106 + if (didExportsChange) {
107 + // In a real module system, we would propagate such updates upwards,
108 + // and re-execute modules that imported this one. (Just like if we edited them.)
109 + // This makes adding/removing/renaming exports re-render references to them.
110 + // Here, we'll just force a re-render using the newer type to emulate this.
111 + const NextComponent = nextExports.default;
112 + act(() => {
113 + ReactDOM.render(<NextComponent />, container);
114 + });
115 + }
116 act(() => {
91 - expect(ReactFreshRuntime.performReactRefresh()).not.toBe(null);
117 + const result = ReactFreshRuntime.performReactRefresh();
118 + if (!didExportsChange) {
119 + // Normally we expect that some components got updated in our tests.
120 + expect(result).not.toBe(null);
121 + } else {
122 + // However, we have tests where we convert functions to classes,
123 + // and in those cases it's expected nothing would get updated.
124 + // (Instead, the export change branch above would take care of it.)
125 + }
126 });
127 expect(ReactFreshRuntime._getMountedRootCount()).toBe(1);
128 }
@@ -945,6 +979,326 @@ describe('ReactFreshIntegration', () => {
979 }
980 });
981
982 + it('remounts when switching export from function to class', () => {
983 + if (__DEV__) {
984 + render(`
985 + export default function App() {
986 + return <h1>A1</h1>;
987 + }
988 + `);
989 + let el = container.firstChild;
990 + expect(el.textContent).toBe('A1');
991 + patch(`
992 + export default function App() {
993 + return <h1>A2</h1>;
994 + }
995 + `);
996 + // Keep state.
997 + expect(container.firstChild).toBe(el);
998 + expect(el.textContent).toBe('A2');
999 +
1000 + patch(`
1001 + export default class App extends React.Component {
1002 + render() {
1003 + return <h1>B1</h1>
1004 + }
1005 + }
1006 + `);
1007 + // Reset (function -> class).
1008 + expect(container.firstChild).not.toBe(el);
1009 + el = container.firstChild;
1010 + expect(el.textContent).toBe('B1');
1011 + patch(`
1012 + export default class App extends React.Component {
1013 + render() {
1014 + return <h1>B2</h1>
1015 + }
1016 + }
1017 + `);
1018 + // Reset (classes always do).
1019 + expect(container.firstChild).not.toBe(el);
1020 + el = container.firstChild;
1021 + expect(el.textContent).toBe('B2');
1022 +
1023 + patch(`
1024 + export default function App() {
1025 + return <h1>C1</h1>;
1026 + }
1027 + `);
1028 + // Reset (class -> function).
1029 + expect(container.firstChild).not.toBe(el);
1030 + el = container.firstChild;
1031 + expect(el.textContent).toBe('C1');
1032 + patch(`
1033 + export default function App() {
1034 + return <h1>C2</h1>;
1035 + }
1036 + `);
1037 + expect(container.firstChild).toBe(el);
1038 + expect(el.textContent).toBe('C2');
1039 +
1040 + patch(`
1041 + export default function App() {
1042 + return <h1>D1</h1>;
1043 + }
1044 + `);
1045 + el = container.firstChild;
1046 + expect(el.textContent).toBe('D1');
1047 + patch(`
1048 + export default function App() {
1049 + return <h1>D2</h1>;
1050 + }
1051 + `);
1052 + // Keep state.
1053 + expect(container.firstChild).toBe(el);
1054 + expect(el.textContent).toBe('D2');
1055 + }
1056 + });
1057 +
1058 + it('remounts when switching export from class to function', () => {
1059 + if (__DEV__) {
1060 + render(`
1061 + export default class App extends React.Component {
1062 + render() {
1063 + return <h1>A1</h1>
1064 + }
1065 + }
1066 + `);
1067 + let el = container.firstChild;
1068 + expect(el.textContent).toBe('A1');
1069 + patch(`
1070 + export default class App extends React.Component {
1071 + render() {
1072 + return <h1>A2</h1>
1073 + }
1074 + }
1075 + `);
1076 + // Reset (classes always do).
1077 + expect(container.firstChild).not.toBe(el);
1078 + el = container.firstChild;
1079 + expect(el.textContent).toBe('A2');
1080 +
1081 + patch(`
1082 + export default function App() {
1083 + return <h1>B1</h1>;
1084 + }
1085 + `);
1086 + // Reset (class -> function).
1087 + expect(container.firstChild).not.toBe(el);
1088 + el = container.firstChild;
1089 + expect(el.textContent).toBe('B1');
1090 + patch(`
1091 + export default function App() {
1092 + return <h1>B2</h1>;
1093 + }
1094 + `);
1095 + // Keep state.
1096 + expect(container.firstChild).toBe(el);
1097 + expect(el.textContent).toBe('B2');
1098 +
1099 + patch(`
1100 + export default class App extends React.Component {
1101 + render() {
1102 + return <h1>C1</h1>
1103 + }
1104 + }
1105 + `);
1106 + // Reset (function -> class).
1107 + expect(container.firstChild).not.toBe(el);
1108 + el = container.firstChild;
1109 + expect(el.textContent).toBe('C1');
1110 + }
1111 + });
1112 +
1113 + it('remounts when wrapping export in a HOC', () => {
1114 + if (__DEV__) {
1115 + render(`
1116 + export default function App() {
1117 + return <h1>A1</h1>;
1118 + }
1119 + `);
1120 + let el = container.firstChild;
1121 + expect(el.textContent).toBe('A1');
1122 + patch(`
1123 + export default function App() {
1124 + return <h1>A2</h1>;
1125 + }
1126 + `);
1127 + // Keep state.
1128 + expect(container.firstChild).toBe(el);
1129 + expect(el.textContent).toBe('A2');
1130 +
1131 + patch(`
1132 + function hoc(Inner) {
1133 + return function Wrapper() {
1134 + return <Inner />;
1135 + }
1136 + }
1137 +
1138 + function App() {
1139 + return <h1>B1</h1>;
1140 + }
1141 +
1142 + export default hoc(App);
1143 + `);
1144 + // Reset (wrapped in HOC).
1145 + expect(container.firstChild).not.toBe(el);
1146 + el = container.firstChild;
1147 + expect(el.textContent).toBe('B1');
1148 + patch(`
1149 + function hoc(Inner) {
1150 + return function Wrapper() {
1151 + return <Inner />;
1152 + }
1153 + }
1154 +
1155 + function App() {
1156 + return <h1>B2</h1>;
1157 + }
1158 +
1159 + export default hoc(App);
1160 + `);
1161 + // Keep state.
1162 + expect(container.firstChild).toBe(el);
1163 + expect(el.textContent).toBe('B2');
1164 +
1165 + patch(`
1166 + export default function App() {
1167 + return <h1>C1</h1>;
1168 + }
1169 + `);
1170 + // Reset (unwrapped).
1171 + expect(container.firstChild).not.toBe(el);
1172 + el = container.firstChild;
1173 + expect(el.textContent).toBe('C1');
1174 + patch(`
1175 + export default function App() {
1176 + return <h1>C2</h1>;
1177 + }
1178 + `);
1179 + expect(container.firstChild).toBe(el);
1180 + expect(el.textContent).toBe('C2');
1181 + }
1182 + });
1183 +
1184 + it('remounts when wrapping export in memo()', () => {
1185 + if (__DEV__) {
1186 + render(`
1187 + export default function App() {
1188 + return <h1>A1</h1>;
1189 + }
1190 + `);
1191 + let el = container.firstChild;
1192 + expect(el.textContent).toBe('A1');
1193 + patch(`
1194 + export default function App() {
1195 + return <h1>A2</h1>;
1196 + }
1197 + `);
1198 + // Keep state.
1199 + expect(container.firstChild).toBe(el);
1200 + expect(el.textContent).toBe('A2');
1201 +
1202 + patch(`
1203 + function App() {
1204 + return <h1>B1</h1>;
1205 + }
1206 +
1207 + export default React.memo(App);
1208 + `);
1209 + // Reset (wrapped in HOC).
1210 + expect(container.firstChild).not.toBe(el);
1211 + el = container.firstChild;
1212 + expect(el.textContent).toBe('B1');
1213 + patch(`
1214 + function App() {
1215 + return <h1>B2</h1>;
1216 + }
1217 +
1218 + export default React.memo(App);
1219 + `);
1220 + // Keep state.
1221 + expect(container.firstChild).toBe(el);
1222 + expect(el.textContent).toBe('B2');
1223 +
1224 + patch(`
1225 + export default function App() {
1226 + return <h1>C1</h1>;
1227 + }
1228 + `);
1229 + // Reset (unwrapped).
1230 + expect(container.firstChild).not.toBe(el);
1231 + el = container.firstChild;
1232 + expect(el.textContent).toBe('C1');
1233 + patch(`
1234 + export default function App() {
1235 + return <h1>C2</h1>;
1236 + }
1237 + `);
1238 + expect(container.firstChild).toBe(el);
1239 + expect(el.textContent).toBe('C2');
1240 + }
1241 + });
1242 +
1243 + it('remounts when wrapping export in forwardRef()', () => {
1244 + if (__DEV__) {
1245 + render(`
1246 + export default function App() {
1247 + return <h1>A1</h1>;
1248 + }
1249 + `);
1250 + let el = container.firstChild;
1251 + expect(el.textContent).toBe('A1');
1252 + patch(`
1253 + export default function App() {
1254 + return <h1>A2</h1>;
1255 + }
1256 + `);
1257 + // Keep state.
1258 + expect(container.firstChild).toBe(el);
1259 + expect(el.textContent).toBe('A2');
1260 +
1261 + patch(`
1262 + function App() {
1263 + return <h1>B1</h1>;
1264 + }
1265 +
1266 + export default React.forwardRef(App);
1267 + `);
1268 + // Reset (wrapped in HOC).
1269 + expect(container.firstChild).not.toBe(el);
1270 + el = container.firstChild;
1271 + expect(el.textContent).toBe('B1');
1272 + patch(`
1273 + function App() {
1274 + return <h1>B2</h1>;
1275 + }
1276 +
1277 + export default React.forwardRef(App);
1278 + `);
1279 + // Keep state.
1280 + expect(container.firstChild).toBe(el);
1281 + expect(el.textContent).toBe('B2');
1282 +
1283 + patch(`
1284 + export default function App() {
1285 + return <h1>C1</h1>;
1286 + }
1287 + `);
1288 + // Reset (unwrapped).
1289 + expect(container.firstChild).not.toBe(el);
1290 + el = container.firstChild;
1291 + expect(el.textContent).toBe('C1');
1292 + patch(`
1293 + export default function App() {
1294 + return <h1>C2</h1>;
1295 + }
1296 + `);
1297 + expect(container.firstChild).toBe(el);
1298 + expect(el.textContent).toBe('C2');
1299 + }
1300 + });
1301 +
1302 describe('with inline requires', () => {
1303 beforeEach(() => {
1304 global.FakeModuleSystem = {};