[DevTools] Validate Store operation invariants (#37050)
Builds on #37049 by validating Store operation invariants before mutation. Missing nodes, invalid element types, inconsistent parent-child relationships, and invalid reorder operations now emit and throw explicit errors instead of silently continuing with corrupted state. Adds a canonical-render regression test for invalid child removal.
Ruslan Lesiutin committed
Jul 23, 2026 at 10:39 UTC
d87711f8d0a45e6950fef15183ad6f9ec827b8b0
2 files changed
+272
-118
packages/react-devtools-shared/src/__tests__/store-test.js
+70
@@ -129,6 +129,76 @@ describe('Store', () => {
129
expect(store).toMatchInlineSnapshot(`[root]`);
130
});
131
132
+ it('throws when a transition timeline is requested during initial paint', () => {
133
+ const errorListener = jest.fn();
134
+ store.addListener('error', errorListener);
135
+
136
+ expect(() =>
137
+ store.getSuspendableDocumentOrderSuspenseTransition(false, 1),
138
+ ).toThrow(
139
+ 'Cannot get a transition timeline during the initial paint. This is a bug in React DevTools.',
140
+ );
141
+ expect(errorListener).toHaveBeenCalledTimes(1);
142
+
143
+ store.removeListener('error', errorListener);
144
+ });
145
+
146
+ // @reactVersion >= 18.0
147
+ it('throws before removing a node that is not a child of its parent', () => {
148
+ function FirstChild() {
149
+ return null;
150
+ }
151
+ function SecondChild() {
152
+ return null;
153
+ }
154
+ function Parent({showFirstChild}) {
155
+ return (
156
+ <>
157
+ {showFirstChild && <FirstChild />}
158
+ <SecondChild />
159
+ </>
160
+ );
161
+ }
162
+
163
+ act(() => render(<Parent showFirstChild={true} />));
164
+
165
+ const parent = store.getElementAtIndex(0);
166
+ expect(parent.displayName).toBe('Parent');
167
+ const firstChildIndex = parent.children.findIndex(id => {
168
+ const child = store.getElementByID(id);
169
+ return child !== null && child.displayName === 'FirstChild';
170
+ });
171
+ expect(firstChildIndex).not.toBe(-1);
172
+ const firstChildID = parent.children[firstChildIndex];
173
+
174
+ // Corrupt only the frontend relationship. The removal operation below is
175
+ // still produced canonically by rendering React.
176
+ parent.children.splice(firstChildIndex, 1);
177
+
178
+ const errorListener = jest.fn();
179
+ store.addListener('error', errorListener);
180
+ let caughtError = null;
181
+ try {
182
+ act(() => render(<Parent showFirstChild={false} />));
183
+ } catch (error) {
184
+ caughtError = error;
185
+ } finally {
186
+ // The test Bridge invokes listeners synchronously, so discard the batch
187
+ // whose Store listener intentionally threw.
188
+ bridge._messageQueue.length = 0;
189
+ }
190
+
191
+ const expectedMessage =
192
+ `Cannot remove node "${firstChildID}" from parent "${parent.id}" ` +
193
+ `because it is not a child of the parent.`;
194
+ expect(caughtError).toMatchObject({message: expectedMessage});
195
+ expect(errorListener).toHaveBeenCalledWith(caughtError);
196
+ expect(store.containsElement(firstChildID)).toBe(true);
197
+
198
+ parent.children.splice(firstChildIndex, 0, firstChildID);
199
+ store.removeListener('error', errorListener);
200
+ });
201
+
202
// This test is not the same cause as what's reported on GitHub,
203
// but the resulting behavior (owner mounting after descendant) is the same.
204
// Thec ase below is admittedly contrived and relies on side effects.
packages/react-devtools-shared/src/devtools/store.js
+202
-118
@@ -28,7 +28,20 @@ import {
28
SUSPENSE_TREE_OPERATION_SUSPENDERS,
29
} from '../constants';
30
import {
31
+ ElementTypeClass,
32
+ ElementTypeContext,
33
+ ElementTypeFunction,
34
+ ElementTypeForwardRef,
35
+ ElementTypeHostComponent,
36
+ ElementTypeMemo,
37
+ ElementTypeOtherOrUnknown,
38
+ ElementTypeProfiler,
39
ElementTypeRoot,
40
+ ElementTypeSuspense,
41
+ ElementTypeSuspenseList,
42
+ ElementTypeTracingMarker,
43
+ ElementTypeVirtual,
44
+ ElementTypeViewTransition,
45
ElementTypeActivity,
46
ComponentFilterActivitySlice,
47
} from '../frontend/types';
@@ -140,6 +153,32 @@ function isNonZeroRect(rect: Rect) {
153
return rect.width > 0 || rect.height > 0 || rect.x > 0 || rect.y > 0;
154
}
155
156
+function parseElementType(value: number): ElementType | null {
157
+ // Cast before switching so Flow checks exhaustiveness while the default rejects unknown bridge values.
158
+ const type = value as any as ElementType;
159
+ switch (type) {
160
+ case ElementTypeClass:
161
+ case ElementTypeContext:
162
+ case ElementTypeFunction:
163
+ case ElementTypeForwardRef:
164
+ case ElementTypeHostComponent:
165
+ case ElementTypeMemo:
166
+ case ElementTypeOtherOrUnknown:
167
+ case ElementTypeProfiler:
168
+ case ElementTypeRoot:
169
+ case ElementTypeSuspense:
170
+ case ElementTypeSuspenseList:
171
+ case ElementTypeTracingMarker:
172
+ case ElementTypeVirtual:
173
+ case ElementTypeViewTransition:
174
+ case ElementTypeActivity:
175
+ return type;
176
+ default:
177
+ (type) as empty;
178
+ return null;
179
+ }
180
+}
181
+
182
/**
183
* The store is the single source of truth for updates from the backend.
184
* ContextProviders can subscribe to the Store for specific things they want to provide.
@@ -366,7 +405,7 @@ export default class Store extends EventEmitter<{
405
}
406
407
// This is only used in tests to avoid memory leaks.
369
- assertMapSizeMatchesRootCount(map: Map<any, any>, mapName: string) {
408
+ assertMapSizeMatchesRootCount<K, V>(map: Map<K, V>, mapName: string) {
409
const expectedSize = this.roots.length;
410
if (map.size !== expectedSize) {
411
this._throwAndEmitError(
@@ -609,13 +648,11 @@ export default class Store extends EventEmitter<{
648
649
if (root === undefined) {
650
// We should never reach this. This is a bug in the backend renderer.
612
- this._throwAndEmitError(
651
+ return this._throwAndEmitError(
652
Error(
653
`Couldn't find root with id "${rootID}": no matching node was found in the Store.`,
654
),
655
);
617
-
618
- return null;
656
}
657
658
if (root.children.length === 0) {
@@ -630,7 +667,9 @@ export default class Store extends EventEmitter<{
667
}
668
669
if (root === undefined) {
633
- return null;
670
+ return this._throwAndEmitError(
671
+ Error(`Could not find an element at index "${index}" in the Store.`),
672
+ );
673
}
674
675
// Find the element in the tree using the weight of each node...
@@ -640,19 +679,18 @@ export default class Store extends EventEmitter<{
679
680
while (index !== currentWeight) {
681
const numChildren = currentElement.children.length;
682
+ let didFindChild = false;
683
for (let i = 0; i < numChildren; i++) {
684
const childID = currentElement.children[i];
685
const child = this._idToElement.get(childID);
686
687
if (child === undefined) {
688
// We should never reach this. This is a bug in the backend renderer.
649
- this._throwAndEmitError(
689
+ return this._throwAndEmitError(
690
Error(
691
`Couldn't child element with id "${childID}": no matching node was found in the Store.`,
692
),
693
);
654
-
655
- return null;
694
}
695
696
const childWeight = child.isCollapsed ? 1 : child.weight;
@@ -660,14 +698,23 @@ export default class Store extends EventEmitter<{
698
if (index <= currentWeight + childWeight) {
699
currentWeight++;
700
currentElement = child;
701
+ didFindChild = true;
702
break;
703
} else {
704
currentWeight += childWeight;
705
}
706
}
707
+
708
+ if (!didFindChild) {
709
+ return this._throwAndEmitError(
710
+ Error(
711
+ `Could not find an element at index "${index}" because the Store tree weights are invalid.`,
712
+ ),
713
+ );
714
+ }
715
}
716
670
- return currentElement || null;
717
+ return currentElement;
718
}
719
720
getElementIDAtIndex(index: number): number | null {
@@ -685,6 +732,26 @@ export default class Store extends EventEmitter<{
732
return element;
733
}
734
735
+ _getElementByIDOrThrow(id: Element['id']): Element {
736
+ const element = this._idToElement.get(id);
737
+ if (element === undefined) {
738
+ return this._throwAndEmitError(
739
+ Error(
740
+ `Could not find element with id "${id}": no matching node was found in the Store.`,
741
+ ),
742
+ );
743
+ }
744
+ return element;
745
+ }
746
+
747
+ _recalculateWeightAcrossRoots(): void {
748
+ let weightAcrossRoots = 0;
749
+ this._roots.forEach(rootID => {
750
+ weightAcrossRoots += this._getElementByIDOrThrow(rootID).weight;
751
+ });
752
+ this._weightAcrossRoots = weightAcrossRoots;
753
+ }
754
+
755
containsSuspense(id: SuspenseNode['id']): boolean {
756
return this._idToSuspense.has(id);
757
}
@@ -893,8 +960,15 @@ export default class Store extends EventEmitter<{
960
let depth = 0;
961
while (parentID > 0) {
962
if (parentID === ownerID || unsortedIDs.has(parentID)) {
896
- // $FlowFixMe[unsafe-addition] addition with possible null/undefined value
897
- depth = depthMap.get(parentID) + 1;
963
+ const parentDepth = depthMap.get(parentID);
964
+ if (parentDepth === undefined) {
965
+ return this._throwAndEmitError(
966
+ Error(
967
+ `Invalid owners list: owner depth for element "${parentID}" was not found.`,
968
+ ),
969
+ );
970
+ }
971
+ depth = parentDepth + 1;
972
depthMap.set(id, depth);
973
break;
974
}
@@ -961,14 +1035,13 @@ export default class Store extends EventEmitter<{
1035
let rootStep: null | SuspenseTimelineStep = null;
1036
for (let i = 0; i < roots.length; i++) {
1037
const rootID = roots[i];
964
- const root = this.getElementByID(rootID);
965
- if (root === null) {
966
- continue;
967
- }
1038
+ this._getElementByIDOrThrow(rootID);
1039
const rendererID = this._rootIDToRendererID.get(rootID);
1040
if (rendererID === undefined) {
970
- throw new Error(
971
- 'Failed to find renderer ID for root. This is a bug in React DevTools.',
1041
+ return this._throwAndEmitError(
1042
+ Error(
1043
+ 'Failed to find renderer ID for root. This is a bug in React DevTools.',
1044
+ ),
1045
);
1046
}
1047
// TODO: This includes boundaries that can't be suspended due to no support from the renderer.
@@ -1059,9 +1132,12 @@ export default class Store extends EventEmitter<{
1132
): Array<SuspenseTimelineStep> {
1133
const target: Array<SuspenseTimelineStep> = [];
1134
const focusedTransitionID = this._focusedTransition;
1062
- // $FlowFixMe[invalid-compare]
1063
- if (focusedTransitionID === null) {
1064
- return target;
1135
+ if (focusedTransitionID === 0) {
1136
+ return this._throwAndEmitError(
1137
+ Error(
1138
+ 'Cannot get a transition timeline during the initial paint. This is a bug in React DevTools.',
1139
+ ),
1140
+ );
1141
}
1142
1143
target.push({
@@ -1155,14 +1231,18 @@ export default class Store extends EventEmitter<{
1231
this._focusedTransition,
1232
);
1233
if (focusedTransitionRootID === null) {
1158
- throw new Error(
1159
- 'Failed to find root ID for focused transition. This is a bug in React DevTools.',
1234
+ return this._throwAndEmitError(
1235
+ Error(
1236
+ 'Failed to find root ID for focused transition. This is a bug in React DevTools.',
1237
+ ),
1238
);
1239
}
1240
const rendererID = this._rootIDToRendererID.get(focusedTransitionRootID);
1241
if (rendererID === undefined) {
1164
- throw new Error(
1165
- 'Failed to find renderer ID for focused transition root. This is a bug in React DevTools.',
1242
+ return this._throwAndEmitError(
1243
+ Error(
1244
+ 'Failed to find renderer ID for focused transition root. This is a bug in React DevTools.',
1245
+ ),
1246
);
1247
}
1248
timeline = this.getSuspendableDocumentOrderSuspenseTransition(
@@ -1313,12 +1393,7 @@ export default class Store extends EventEmitter<{
1393
1394
// Only re-calculate weights and emit an "update" event if the store was mutated.
1395
if (didMutate) {
1316
- let weightAcrossRoots = 0;
1317
- this._roots.forEach(rootID => {
1318
- const {weight} = this.getElementByID(rootID) as any as Element;
1319
- weightAcrossRoots += weight;
1320
- });
1321
- this._weightAcrossRoots = weightAcrossRoots;
1396
+ this._recalculateWeightAcrossRoots();
1397
1398
// The Tree context's search reducer expects an explicit list of ids for nodes that were added or removed.
1399
// In this case, we can pass it empty arrays since nodes in a collapsed tree are still there (just hidden).
@@ -1428,13 +1503,22 @@ export default class Store extends EventEmitter<{
1503
switch (operation) {
1504
case TREE_OPERATION_ADD: {
1505
const id = operations[i + 1];
1431
- const type = operations[i + 2] as any as ElementType;
1506
+ const rawType = operations[i + 2];
1507
+ const type = parseElementType(rawType);
1508
+
1509
+ if (type === null) {
1510
+ return this._throwAndEmitError(
1511
+ Error(
1512
+ `Cannot add node "${id}" because "${rawType}" is not a valid element type.`,
1513
+ ),
1514
+ );
1515
+ }
1516
1517
i += 3;
1518
1519
if (this._idToElement.has(id)) {
1520
// We should never reach this. This is a bug in the backend renderer.
1437
- this._throwAndEmitError(
1521
+ return this._throwAndEmitError(
1522
Error(
1523
`Cannot add node "${id}" because a node with that id is already in the Store.`,
1524
),
@@ -1545,13 +1629,11 @@ export default class Store extends EventEmitter<{
1629
const parentElement = this._idToElement.get(parentID);
1630
if (parentElement === undefined) {
1631
// We should never reach this. This is a bug in the backend renderer.
1548
- this._throwAndEmitError(
1632
+ return this._throwAndEmitError(
1633
Error(
1634
`Cannot add child "${id}" to parent "${parentID}" because parent node was not found in the Store.`,
1635
),
1636
);
1553
-
1554
- break;
1637
}
1638
1639
parentElement.children.push(id);
@@ -1622,13 +1704,11 @@ export default class Store extends EventEmitter<{
1704
1705
if (element === undefined) {
1706
// We should never reach this. This is a bug in the backend renderer.
1625
- this._throwAndEmitError(
1707
+ return this._throwAndEmitError(
1708
Error(
1709
`Cannot remove node "${id}" because no matching node was found in the Store.`,
1710
),
1711
);
1630
-
1631
- break;
1712
}
1713
1714
i += 1;
@@ -1636,13 +1716,11 @@ export default class Store extends EventEmitter<{
1716
const {children, ownerID, parentID, weight} = element;
1717
if (children.length > 0) {
1718
// We should never reach this. This is a bug in the backend renderer.
1639
- this._throwAndEmitError(
1719
+ return this._throwAndEmitError(
1720
Error(`Node "${id}" was removed before its children.`),
1721
);
1722
}
1723
1644
- this._idToElement.delete(id);
1645
-
1724
let parentElement: ?Element = null;
1725
if (parentID === 0) {
1726
// $FlowFixMe[constant-condition]
@@ -1664,19 +1742,26 @@ export default class Store extends EventEmitter<{
1742
parentElement = this._idToElement.get(parentID);
1743
if (parentElement === undefined) {
1744
// We should never reach this. This is a bug in the backend renderer.
1667
- this._throwAndEmitError(
1745
+ return this._throwAndEmitError(
1746
Error(
1747
`Cannot remove node "${id}" from parent "${parentID}" because no matching node was found in the Store.`,
1748
),
1749
);
1672
-
1673
- break;
1750
}
1751
1752
const index = parentElement.children.indexOf(id);
1753
+ if (index === -1) {
1754
+ return this._throwAndEmitError(
1755
+ Error(
1756
+ `Cannot remove node "${id}" from parent "${parentID}" because it is not a child of the parent.`,
1757
+ ),
1758
+ );
1759
+ }
1760
parentElement.children.splice(index, 1);
1761
}
1762
1763
+ this._idToElement.delete(id);
1764
+
1765
this._adjustParentTreeWeight(parentElement, -weight);
1766
removedElementIDs.set(id, parentID);
1767
@@ -1704,37 +1789,42 @@ export default class Store extends EventEmitter<{
1789
const element = this._idToElement.get(id);
1790
if (element === undefined) {
1791
// We should never reach this. This is a bug in the backend renderer.
1707
- this._throwAndEmitError(
1792
+ return this._throwAndEmitError(
1793
Error(
1794
`Cannot reorder children for node "${id}" because no matching node was found in the Store.`,
1795
),
1796
);
1712
-
1713
- break;
1797
}
1798
1799
const children = element.children;
1800
if (children.length !== numChildren) {
1801
// We should never reach this. This is a bug in the backend renderer.
1719
- this._throwAndEmitError(
1802
+ return this._throwAndEmitError(
1803
Error(
1804
`Children cannot be added or removed during a reorder operation.`,
1805
),
1806
);
1807
}
1808
1809
+ const reorderedChildIDs: Set<Element['id']> = new Set();
1810
for (let j = 0; j < numChildren; j++) {
1811
const childID = operations[i + j];
1728
- children[j] = childID;
1729
- if (__DEV__) {
1730
- // This check is more expensive so it's gated by __DEV__.
1731
- const childElement = this._idToElement.get(childID);
1732
- if (childElement == null || childElement.parentID !== id) {
1733
- console.error(
1812
+ const childElement = this._idToElement.get(childID);
1813
+ if (
1814
+ childElement === undefined ||
1815
+ childElement.parentID !== id ||
1816
+ reorderedChildIDs.has(childID)
1817
+ ) {
1818
+ return this._throwAndEmitError(
1819
+ Error(
1820
`Children cannot be added or removed during a reorder operation.`,
1735
- );
1736
- }
1821
+ ),
1822
+ );
1823
}
1824
+ reorderedChildIDs.add(childID);
1825
+ }
1826
+ for (let j = 0; j < numChildren; j++) {
1827
+ children[j] = operations[i + j];
1828
}
1829
i += numChildren;
1830
@@ -1829,12 +1919,12 @@ export default class Store extends EventEmitter<{
1919
const parentID = operations[i + 2];
1920
const nameStringID = operations[i + 3];
1921
const isSuspended = operations[i + 4] === 1;
1832
- const numRects = operations[i + 5] as any as number;
1922
+ const numRects = operations[i + 5];
1923
let name = stringTable[nameStringID];
1924
1925
if (this._idToSuspense.has(id)) {
1926
// We should never reach this. This is a bug in the backend renderer.
1837
- this._throwAndEmitError(
1927
+ return this._throwAndEmitError(
1928
Error(
1929
`Cannot add suspense node "${id}" because a suspense node with that id is already in the Store.`,
1930
),
@@ -1887,13 +1977,11 @@ export default class Store extends EventEmitter<{
1977
const parentSuspense = this._idToSuspense.get(parentID);
1978
if (parentSuspense === undefined) {
1979
// We should never reach this. This is a bug in the backend renderer.
1890
- this._throwAndEmitError(
1980
+ return this._throwAndEmitError(
1981
Error(
1982
`Cannot add suspense child "${id}" to parent suspense "${parentID}" because parent suspense node was not found in the Store.`,
1983
),
1984
);
1895
-
1896
- break;
1985
}
1986
1987
parentSuspense.children.push(id);
@@ -1924,13 +2012,11 @@ export default class Store extends EventEmitter<{
2012
2013
if (suspense === undefined) {
2014
// We should never reach this. This is a bug in the backend renderer.
1927
- this._throwAndEmitError(
2015
+ return this._throwAndEmitError(
2016
Error(
2017
`Cannot remove suspense node "${id}" because no matching node was found in the Store.`,
2018
),
2019
);
1932
-
1933
- break;
2020
}
2021
2022
i += 1;
@@ -1938,11 +2024,33 @@ export default class Store extends EventEmitter<{
2024
const {children, parentID, rects} = suspense;
2025
if (children.length > 0) {
2026
// We should never reach this. This is a bug in the backend renderer.
1941
- this._throwAndEmitError(
2027
+ return this._throwAndEmitError(
2028
Error(`Suspense node "${id}" was removed before its children.`),
2029
);
2030
}
2031
2032
+ let parentSuspense: SuspenseNode | null = null;
2033
+ let parentIndex = -1;
2034
+ if (parentID !== 0) {
2035
+ parentSuspense = this._idToSuspense.get(parentID) || null;
2036
+ if (parentSuspense === null) {
2037
+ return this._throwAndEmitError(
2038
+ Error(
2039
+ `Cannot remove suspense node "${id}" from parent "${parentID}" because no matching node was found in the Store.`,
2040
+ ),
2041
+ );
2042
+ }
2043
+
2044
+ parentIndex = parentSuspense.children.indexOf(id);
2045
+ if (parentIndex === -1) {
2046
+ return this._throwAndEmitError(
2047
+ Error(
2048
+ `Cannot remove suspense node "${id}" from parent "${parentID}" because it is not a child of the parent.`,
2049
+ ),
2050
+ );
2051
+ }
2052
+ }
2053
+
2054
if (rects !== null && parentID !== 0) {
2055
// Delete all the existing rects from the R-tree
2056
for (let j = 0; j < rects.length; j++) {
@@ -1953,8 +2061,7 @@ export default class Store extends EventEmitter<{
2061
this._idToSuspense.delete(id);
2062
removedSuspenseIDs.set(id, parentID);
2063
1956
- let parentSuspense: ?SuspenseNode = null;
1957
- if (parentID === 0) {
2064
+ if (parentSuspense === null) {
2065
// $FlowFixMe[constant-condition]
2066
if (__DEBUG__) {
2067
debug('Suspense remove', `node ${id} root`);
@@ -1965,28 +2072,7 @@ export default class Store extends EventEmitter<{
2072
debug('Suspense Remove', `node ${id} from parent ${parentID}`);
2073
}
2074
1968
- parentSuspense = this._idToSuspense.get(parentID);
1969
- if (parentSuspense === undefined) {
1970
- // We should never reach this. This is a bug in the backend renderer.
1971
- this._throwAndEmitError(
1972
- Error(
1973
- `Cannot remove suspense node "${id}" from parent "${parentID}" because no matching node was found in the Store.`,
1974
- ),
1975
- );
1976
-
1977
- break;
1978
- }
1979
-
1980
- const index = parentSuspense.children.indexOf(id);
1981
- if (index === -1) {
1982
- // We should never reach this. This is a bug in the backend renderer.
1983
- this._throwAndEmitError(
1984
- Error(
1985
- `Cannot remove suspense node "${id}" from parent "${parentID}" because it is not a child of the parent.`,
1986
- ),
1987
- );
1988
- }
1989
- parentSuspense.children.splice(index, 1);
2075
+ parentSuspense.children.splice(parentIndex, 1);
2076
}
2077
}
2078
@@ -2001,37 +2087,42 @@ export default class Store extends EventEmitter<{
2087
const suspense = this._idToSuspense.get(id);
2088
if (suspense === undefined) {
2089
// We should never reach this. This is a bug in the backend renderer.
2004
- this._throwAndEmitError(
2090
+ return this._throwAndEmitError(
2091
Error(
2092
`Cannot reorder children for suspense node "${id}" because no matching node was found in the Store.`,
2093
),
2094
);
2009
-
2010
- break;
2095
}
2096
2097
const children = suspense.children;
2098
if (children.length !== numChildren) {
2099
// We should never reach this. This is a bug in the backend renderer.
2016
- this._throwAndEmitError(
2100
+ return this._throwAndEmitError(
2101
Error(
2102
`Suspense children cannot be added or removed during a reorder operation.`,
2103
),
2104
);
2105
}
2106
2107
+ const reorderedChildIDs: Set<SuspenseNode['id']> = new Set();
2108
for (let j = 0; j < numChildren; j++) {
2109
const childID = operations[i + j];
2025
- children[j] = childID;
2026
- if (__DEV__) {
2027
- // This check is more expensive so it's gated by __DEV__.
2028
- const childSuspense = this._idToSuspense.get(childID);
2029
- if (childSuspense == null || childSuspense.parentID !== id) {
2030
- console.error(
2110
+ const childSuspense = this._idToSuspense.get(childID);
2111
+ if (
2112
+ childSuspense === undefined ||
2113
+ childSuspense.parentID !== id ||
2114
+ reorderedChildIDs.has(childID)
2115
+ ) {
2116
+ return this._throwAndEmitError(
2117
+ Error(
2118
`Suspense children cannot be added or removed during a reorder operation.`,
2032
- );
2033
- }
2119
+ ),
2120
+ );
2121
}
2122
+ reorderedChildIDs.add(childID);
2123
+ }
2124
+ for (let j = 0; j < numChildren; j++) {
2125
+ children[j] = operations[i + j];
2126
}
2127
i += numChildren;
2128
@@ -2047,20 +2138,18 @@ export default class Store extends EventEmitter<{
2138
break;
2139
}
2140
case SUSPENSE_TREE_OPERATION_RESIZE: {
2050
- const id = operations[i + 1] as any as number;
2051
- const numRects = operations[i + 2] as any as number;
2141
+ const id = operations[i + 1];
2142
+ const numRects = operations[i + 2];
2143
i += 3;
2144
2145
const suspense = this._idToSuspense.get(id);
2146
if (suspense === undefined) {
2147
// We should never reach this. This is a bug in the backend renderer.
2057
- this._throwAndEmitError(
2148
+ return this._throwAndEmitError(
2149
Error(
2150
`Cannot set rects for suspense node "${id}" because no matching node was found in the Store.`,
2151
),
2152
);
2062
-
2063
- break;
2153
}
2154
2155
const prevRects = suspense.rects;
@@ -2142,13 +2231,11 @@ export default class Store extends EventEmitter<{
2231
2232
if (suspense === undefined) {
2233
// We should never reach this. This is a bug in the backend renderer.
2145
- this._throwAndEmitError(
2234
+ return this._throwAndEmitError(
2235
Error(
2236
`Cannot update suspenders of suspense node "${id}" because no matching node was found in the Store.`,
2237
),
2238
);
2150
-
2151
- break;
2239
}
2240
2241
// $FlowFixMe[constant-condition]
@@ -2178,7 +2265,7 @@ export default class Store extends EventEmitter<{
2265
break;
2266
}
2267
default:
2181
- this._throwAndEmitError(
2268
+ return this._throwAndEmitError(
2269
new UnsupportedBridgeOperationError(
2270
`Unsupported Bridge operation "${operation}"`,
2271
),
@@ -2283,7 +2370,9 @@ export default class Store extends EventEmitter<{
2370
// the Activities that are descendants of the next Activity slice.
2371
const nextActivitySlice = this._idToElement.get(nextActivitySliceID);
2372
if (nextActivitySlice === undefined) {
2286
- throw new Error('Next Activity slice not found in Store.');
2373
+ return this._throwAndEmitError(
2374
+ Error('Next Activity slice not found in Store.'),
2375
+ );
2376
}
2377
2378
for (let j = 0; j < nextActivitySlice.children.length; j++) {
@@ -2293,12 +2382,7 @@ export default class Store extends EventEmitter<{
2382
}
2383
2384
if (didCollapse) {
2296
- let weightAcrossRoots = 0;
2297
- this._roots.forEach(rootID => {
2298
- const {weight} = this.getElementByID(rootID) as any as Element;
2299
- weightAcrossRoots += weight;
2300
- });
2301
- this._weightAcrossRoots = weightAcrossRoots;
2385
+ this._recalculateWeightAcrossRoots();
2386
}
2387
}
2388
@@ -2329,7 +2413,7 @@ export default class Store extends EventEmitter<{
2413
let didMutate = false;
2414
const element = this._idToElement.get(elementID);
2415
if (element === undefined) {
2332
- throw new Error('Element not found in Store.');
2416
+ return this._throwAndEmitError(Error('Element not found in Store.'));
2417
}
2418
2419
if (element.type === ElementTypeActivity) {