Skip to content

Commit efade36

Browse files
author
Brian Vaughn
committed
Improve React error message when mutable sources are mutated during render
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. I think this is the best we can do without adding production overhead that we'd probably prefer to avoid.
1 parent a511dc7 commit efade36

File tree

6 files changed

+132
-4
lines changed

6 files changed

+132
-4
lines changed

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,18 @@ function readFromUnsubcribedMutableSource<Source, Snapshot>(
892892
const getVersion = source._getVersion;
893893
const version = getVersion(source._source);
894894

895+
let mutableSourceSideEffectDetected = false;
896+
if (__DEV__) {
897+
// Detect side effects that update a mutable source during render.
898+
// See https://github.com/facebook/react/issues/19948
899+
if (source._currentlyRenderingFiber !== currentlyRenderingFiber) {
900+
source._currentlyRenderingFiber = currentlyRenderingFiber;
901+
source._initialVersionAsOfFirstRender = version;
902+
} else if (source._initialVersionAsOfFirstRender !== version) {
903+
mutableSourceSideEffectDetected = true;
904+
}
905+
}
906+
895907
// Is it safe for this component to read from this source during the current render?
896908
let isSafeToReadFromSource = false;
897909

@@ -954,9 +966,17 @@ function readFromUnsubcribedMutableSource<Source, Snapshot>(
954966
// but there's nothing we can do about that (short of throwing here and refusing to continue the render).
955967
markSourceAsDirty(source);
956968

969+
if (__DEV__) {
970+
if (mutableSourceSideEffectDetected) {
971+
console.warn(
972+
'React detected an unsafe side effect during render. Mutable sources should not be modified during render.',
973+
);
974+
}
975+
}
976+
957977
invariant(
958978
false,
959-
'Cannot read from mutable source during the current render without tearing. This is a bug in React. Please file an issue.',
979+
'Cannot read from mutable source during the current render without tearing. This may be a bug in React. Please file an issue.',
960980
);
961981
}
962982
}

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,18 @@ function readFromUnsubcribedMutableSource<Source, Snapshot>(
871871
const getVersion = source._getVersion;
872872
const version = getVersion(source._source);
873873

874+
let mutableSourceSideEffectDetected = false;
875+
if (__DEV__) {
876+
// Detect side effects that update a mutable source during render.
877+
// See https://github.com/facebook/react/issues/19948
878+
if (source._currentlyRenderingFiber !== currentlyRenderingFiber) {
879+
source._currentlyRenderingFiber = currentlyRenderingFiber;
880+
source._initialVersionAsOfFirstRender = version;
881+
} else if (source._initialVersionAsOfFirstRender !== version) {
882+
mutableSourceSideEffectDetected = true;
883+
}
884+
}
885+
874886
// Is it safe for this component to read from this source during the current render?
875887
let isSafeToReadFromSource = false;
876888

@@ -933,9 +945,17 @@ function readFromUnsubcribedMutableSource<Source, Snapshot>(
933945
// but there's nothing we can do about that (short of throwing here and refusing to continue the render).
934946
markSourceAsDirty(source);
935947

948+
if (__DEV__) {
949+
if (mutableSourceSideEffectDetected) {
950+
console.warn(
951+
'React detected an unsafe side effect during render. Mutable sources should not be modified during render.',
952+
);
953+
}
954+
}
955+
936956
invariant(
937957
false,
938-
'Cannot read from mutable source during the current render without tearing. This is a bug in React. Please file an issue.',
958+
'Cannot read from mutable source during the current render without tearing. This may be a bug in React. Please file an issue.',
939959
);
940960
}
941961
}

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

Lines changed: 77 additions & 1 deletion
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');
@@ -1719,6 +1719,82 @@ describe('useMutableSource', () => {
17191719
});
17201720

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

packages/react/src/ReactMutableSource.js

Lines changed: 5 additions & 0 deletions
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

Lines changed: 6 additions & 0 deletions
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

Lines changed: 2 additions & 1 deletion
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)