[Fresh] Fix an infinite loop in an edge case (#17414)
* [Fresh] Fix an infinite loop in an edge case * Make it work in IE11
Dan Abramov committed
Nov 21, 2019 at 14:10 UTC
237a966da058fa063abd2e1365e90f2a26aa4e51
2 files changed
+84
-4
packages/react-refresh/src/ReactFreshRuntime.js
+29
-4
@@ -154,6 +154,22 @@ function resolveFamily(type) {
154
return updatedFamiliesByType.get(type);
155
}
156
157
+// If we didn't care about IE11, we could use new Map/Set(iterable).
158
+function cloneMap<K, V>(map: Map<K, V>): Map<K, V> {
159
+ let clone = new Map();
160
+ map.forEach((value, key) => {
161
+ clone.set(key, value);
162
+ });
163
+ return clone;
164
+}
165
+function cloneSet<T>(set: Set<T>): Set<T> {
166
+ let clone = new Set();
167
+ set.forEach(value => {
168
+ clone.add(value);
169
+ });
170
+ return clone;
171
+}
172
+
173
export function performReactRefresh(): RefreshUpdate | null {
174
if (__DEV__) {
175
if (pendingUpdates.length === 0) {
@@ -195,8 +211,17 @@ export function performReactRefresh(): RefreshUpdate | null {
211
212
let didError = false;
213
let firstError = null;
198
- failedRoots.forEach((element, root) => {
199
- const helpers = helpersByRoot.get(root);
214
+
215
+ // We snapshot maps and sets that are mutated during commits.
216
+ // If we don't do this, there is a risk they will be mutated while
217
+ // we iterate over them. For example, trying to recover a failed root
218
+ // may cause another root to be added to the failed list -- an infinite loop.
219
+ let failedRootsSnapshot = cloneMap(failedRoots);
220
+ let mountedRootsSnapshot = cloneSet(mountedRoots);
221
+ let helpersByRootSnapshot = cloneMap(helpersByRoot);
222
+
223
+ failedRootsSnapshot.forEach((element, root) => {
224
+ const helpers = helpersByRootSnapshot.get(root);
225
if (helpers === undefined) {
226
throw new Error(
227
'Could not find helpers for a root. This is a bug in React Refresh.',
@@ -212,8 +237,8 @@ export function performReactRefresh(): RefreshUpdate | null {
237
// Keep trying other roots.
238
}
239
});
215
- mountedRoots.forEach(root => {
216
- const helpers = helpersByRoot.get(root);
240
+ mountedRootsSnapshot.forEach(root => {
241
+ const helpers = helpersByRootSnapshot.get(root);
242
if (helpers === undefined) {
243
throw new Error(
244
'Could not find helpers for a root. This is a bug in React Refresh.',
packages/react-refresh/src/__tests__/ReactFresh-test.js
+55
@@ -2909,6 +2909,61 @@ describe('ReactFresh', () => {
2909
}
2910
});
2911
2912
+ it('regression test: does not get into an infinite loop', () => {
2913
+ if (__DEV__) {
2914
+ let containerA = document.createElement('div');
2915
+ let containerB = document.createElement('div');
2916
+
2917
+ // Initially, nothing interesting.
2918
+ let RootAV1 = () => {
2919
+ return 'A1';
2920
+ };
2921
+ $RefreshReg$(RootAV1, 'RootA');
2922
+ let RootBV1 = () => {
2923
+ return 'B1';
2924
+ };
2925
+ $RefreshReg$(RootBV1, 'RootB');
2926
+
2927
+ act(() => {
2928
+ ReactDOM.render(<RootAV1 />, containerA);
2929
+ ReactDOM.render(<RootBV1 />, containerB);
2930
+ });
2931
+ expect(containerA.innerHTML).toBe('A1');
2932
+ expect(containerB.innerHTML).toBe('B1');
2933
+
2934
+ // Then make the first root fail.
2935
+ let RootAV2 = () => {
2936
+ throw new Error('A2!');
2937
+ };
2938
+ $RefreshReg$(RootAV2, 'RootA');
2939
+ expect(() => ReactFreshRuntime.performReactRefresh()).toThrow('A2!');
2940
+ expect(containerA.innerHTML).toBe('');
2941
+ expect(containerB.innerHTML).toBe('B1');
2942
+
2943
+ // Then patch the first root, but make it fail in the commit phase.
2944
+ // This used to trigger an infinite loop due to a list of failed roots
2945
+ // being mutated while it was being iterated on.
2946
+ let RootAV3 = () => {
2947
+ React.useLayoutEffect(() => {
2948
+ throw new Error('A3!');
2949
+ }, []);
2950
+ return 'A3';
2951
+ };
2952
+ $RefreshReg$(RootAV3, 'RootA');
2953
+ expect(() => ReactFreshRuntime.performReactRefresh()).toThrow('A3!');
2954
+ expect(containerA.innerHTML).toBe('');
2955
+ expect(containerB.innerHTML).toBe('B1');
2956
+
2957
+ let RootAV4 = () => {
2958
+ return 'A4';
2959
+ };
2960
+ $RefreshReg$(RootAV4, 'RootA');
2961
+ ReactFreshRuntime.performReactRefresh();
2962
+ expect(containerA.innerHTML).toBe('A4');
2963
+ expect(containerB.innerHTML).toBe('B1');
2964
+ }
2965
+ });
2966
+
2967
it('remounts classes on every edit', () => {
2968
if (__DEV__) {
2969
let HelloV1 = render(() => {