Skip to content

Commit 766a7a2

Browse files
author
Brian Vaughn
authored
Improve React error message when mutable sources are mutated during render (#20665)
Changed previous error message from: > Cannot read from mutable source during the current render without tearing. This is a bug in React. Please file an issue. To: > Cannot read from mutable source during the current render without tearing. This may be a bug in React. Please file an issue. Also added a DEV only warning about the unsafe side effect: > A mutable source was mutated while the %s component was rendering. This is not supported. Move any mutations into event handlers or effects. I think this is the best we can do without adding production overhead that we'd probably prefer to avoid.
1 parent a922f1c commit 766a7a2

File tree

6 files changed

+140
-4
lines changed

6 files changed

+140
-4
lines changed

packages/react-reconciler/src/ReactFiberHooks.new.js

+24-1
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,18 @@ function readFromUnsubcribedMutableSource<Source, Snapshot>(
904904
const getVersion = source._getVersion;
905905
const version = getVersion(source._source);
906906

907+
let mutableSourceSideEffectDetected = false;
908+
if (__DEV__) {
909+
// Detect side effects that update a mutable source during render.
910+
// See https://github.com/facebook/react/issues/19948
911+
if (source._currentlyRenderingFiber !== currentlyRenderingFiber) {
912+
source._currentlyRenderingFiber = currentlyRenderingFiber;
913+
source._initialVersionAsOfFirstRender = version;
914+
} else if (source._initialVersionAsOfFirstRender !== version) {
915+
mutableSourceSideEffectDetected = true;
916+
}
917+
}
918+
907919
// Is it safe for this component to read from this source during the current render?
908920
let isSafeToReadFromSource = false;
909921

@@ -966,9 +978,20 @@ function readFromUnsubcribedMutableSource<Source, Snapshot>(
966978
// but there's nothing we can do about that (short of throwing here and refusing to continue the render).
967979
markSourceAsDirty(source);
968980

981+
if (__DEV__) {
982+
if (mutableSourceSideEffectDetected) {
983+
const componentName = getComponentName(currentlyRenderingFiber.type);
984+
console.warn(
985+
'A mutable source was mutated while the %s component was rendering. This is not supported. ' +
986+
'Move any mutations into event handlers or effects.',
987+
componentName,
988+
);
989+
}
990+
}
991+
969992
invariant(
970993
false,
971-
'Cannot read from mutable source during the current render without tearing. This is a bug in React. Please file an issue.',
994+
'Cannot read from mutable source during the current render without tearing. This may be a bug in React. Please file an issue.',
972995
);
973996
}
974997
}

packages/react-reconciler/src/ReactFiberHooks.old.js

+24-1
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,18 @@ function readFromUnsubcribedMutableSource<Source, Snapshot>(
885885
const getVersion = source._getVersion;
886886
const version = getVersion(source._source);
887887

888+
let mutableSourceSideEffectDetected = false;
889+
if (__DEV__) {
890+
// Detect side effects that update a mutable source during render.
891+
// See https://github.com/facebook/react/issues/19948
892+
if (source._currentlyRenderingFiber !== currentlyRenderingFiber) {
893+
source._currentlyRenderingFiber = currentlyRenderingFiber;
894+
source._initialVersionAsOfFirstRender = version;
895+
} else if (source._initialVersionAsOfFirstRender !== version) {
896+
mutableSourceSideEffectDetected = true;
897+
}
898+
}
899+
888900
// Is it safe for this component to read from this source during the current render?
889901
let isSafeToReadFromSource = false;
890902

@@ -947,9 +959,20 @@ function readFromUnsubcribedMutableSource<Source, Snapshot>(
947959
// but there's nothing we can do about that (short of throwing here and refusing to continue the render).
948960
markSourceAsDirty(source);
949961

962+
if (__DEV__) {
963+
if (mutableSourceSideEffectDetected) {
964+
const componentName = getComponentName(currentlyRenderingFiber.type);
965+
console.warn(
966+
'A mutable source was mutated while the %s component was rendering. This is not supported. ' +
967+
'Move any mutations into event handlers or effects.',
968+
componentName,
969+
);
970+
}
971+
}
972+
950973
invariant(
951974
false,
952-
'Cannot read from mutable source during the current render without tearing. This is a bug in React. Please file an issue.',
975+
'Cannot read from mutable source during the current render without tearing. This may be a bug in React. Please file an issue.',
953976
);
954977
}
955978
}

packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js

+79-1
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ function loadModules() {
2525
jest.useFakeTimers();
2626

2727
ReactFeatureFlags = require('shared/ReactFeatureFlags');
28-
2928
ReactFeatureFlags.enableSchedulerTracing = true;
3029
ReactFeatureFlags.enableProfilerTimer = true;
30+
3131
React = require('react');
3232
ReactNoop = require('react-noop-renderer');
3333
Scheduler = require('scheduler');
@@ -1720,6 +1720,84 @@ describe('useMutableSource', () => {
17201720
});
17211721

17221722
if (__DEV__) {
1723+
// See https://github.com/facebook/react/issues/19948
1724+
describe('side effecte detection', () => {
1725+
// @gate experimental
1726+
it('should throw if a mutable source is mutated during render', () => {
1727+
const source = createSource('initial');
1728+
const mutableSource = createMutableSource(
1729+
source,
1730+
param => param.version,
1731+
);
1732+
1733+
function MutateDuringRead() {
1734+
const value = useMutableSource(
1735+
mutableSource,
1736+
defaultGetSnapshot,
1737+
defaultSubscribe,
1738+
);
1739+
Scheduler.unstable_yieldValue('MutateDuringRead:' + value);
1740+
// Note that mutating an exeternal value during render is a side effect and is not supported.
1741+
if (value === 'initial') {
1742+
source.value = 'updated';
1743+
}
1744+
return null;
1745+
}
1746+
1747+
expect(() => {
1748+
expect(() => {
1749+
act(() => {
1750+
ReactNoop.renderLegacySyncRoot(
1751+
<React.StrictMode>
1752+
<MutateDuringRead />
1753+
</React.StrictMode>,
1754+
);
1755+
});
1756+
}).toThrow(
1757+
'Cannot read from mutable source during the current render without tearing. This may be a bug in React. Please file an issue.',
1758+
);
1759+
}).toWarnDev(
1760+
'A mutable source was mutated while the MutateDuringRead component was rendering. This is not supported. ' +
1761+
'Move any mutations into event handlers or effects.\n' +
1762+
' in MutateDuringRead (at **)',
1763+
);
1764+
1765+
expect(Scheduler).toHaveYielded(['MutateDuringRead:initial']);
1766+
});
1767+
1768+
// @gate experimental
1769+
it('should not misidentify mutations after render as side effects', () => {
1770+
const source = createSource('initial');
1771+
const mutableSource = createMutableSource(
1772+
source,
1773+
param => param.version,
1774+
);
1775+
1776+
function MutateDuringRead() {
1777+
const value = useMutableSource(
1778+
mutableSource,
1779+
defaultGetSnapshot,
1780+
defaultSubscribe,
1781+
);
1782+
Scheduler.unstable_yieldValue('MutateDuringRead:' + value);
1783+
return null;
1784+
}
1785+
1786+
act(() => {
1787+
ReactNoop.renderLegacySyncRoot(
1788+
<React.StrictMode>
1789+
<MutateDuringRead />
1790+
</React.StrictMode>,
1791+
);
1792+
expect(Scheduler).toFlushAndYieldThrough([
1793+
'MutateDuringRead:initial',
1794+
]);
1795+
source.value = 'updated';
1796+
});
1797+
expect(Scheduler).toHaveYielded(['MutateDuringRead:updated']);
1798+
});
1799+
});
1800+
17231801
describe('dev warnings', () => {
17241802
// @gate experimental
17251803
it('should warn if the subscribe function does not return an unsubscribe function', () => {

packages/react/src/ReactMutableSource.js

+5
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ export function createMutableSource<Source: $NonMaybeType<mixed>>(
2323
if (__DEV__) {
2424
mutableSource._currentPrimaryRenderer = null;
2525
mutableSource._currentSecondaryRenderer = null;
26+
27+
// Used to detect side effects that update a mutable source during render.
28+
// See https://github.com/facebook/react/issues/19948
29+
mutableSource._currentlyRenderingFiber = null;
30+
mutableSource._initialVersionAsOfFirstRender = null;
2631
}
2732

2833
return mutableSource;

packages/shared/ReactTypes.js

+6
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,12 @@ export type MutableSource<Source: $NonMaybeType<mixed>> = {|
197197
// Used to detect multiple renderers using the same mutable source.
198198
_currentPrimaryRenderer?: Object | null,
199199
_currentSecondaryRenderer?: Object | null,
200+
201+
// DEV only
202+
// Used to detect side effects that update a mutable source during render.
203+
// See https://github.com/facebook/react/issues/19948
204+
_currentlyRenderingFiber?: Fiber | null,
205+
_initialVersionAsOfFirstRender?: MutableSourceVersion | null,
200206
|};
201207

202208
// The subset of a Thenable required by things thrown by Suspense.

scripts/error-codes/codes.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -372,5 +372,6 @@
372372
"381": "This feature is not supported by ReactSuspenseTestUtils.",
373373
"382": "This query has received more parameters than the last time the same query was used. Always pass the exact number of parameters that the query needs.",
374374
"383": "This query has received fewer parameters than the last time the same query was used. Always pass the exact number of parameters that the query needs.",
375-
"384": "Refreshing the cache is not supported in Server Components."
375+
"384": "Refreshing the cache is not supported in Server Components.",
376+
"385": "Cannot read from mutable source during the current render without tearing. This may be a bug in React. Please file an issue."
376377
}

0 commit comments

Comments
 (0)