@samitouri / QOS-React-2 / commits / 237a966da0

[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(() => {