mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Warn if MutableSource snapshot is a function (#18933)
* Warn if MutableSource snapshot is a function
useMutableSource does not properly support snapshots that are functions. In part this is because of how it is implemented internally (the function gets mistaken for a state updater function). To fix this we could just wrap another function around the returned snapshot, but this pattern seems problematic to begin with- because the function that gets returned might itself close over mutable values, which would defeat the purpose of using the hook in the first place.
This PR proposes adding a new DEV warning if the snapshot returned is a function. It does not change the behavior (meaning that a function could still work in some cases- but at least the current behavior prevents passing around a closure that may later become stale unless you're really intentional about it e.g. () => () => {...}).
* Replaced .warn with .error
This commit is contained in:
@@ -914,7 +914,16 @@ function readFromUnsubcribedMutableSource<Source, Snapshot>(
|
||||
}
|
||||
|
||||
if (isSafeToReadFromSource) {
|
||||
return getSnapshot(source._source);
|
||||
const snapshot = getSnapshot(source._source);
|
||||
if (__DEV__) {
|
||||
if (typeof snapshot === 'function') {
|
||||
console.error(
|
||||
'Mutable source should not return a function as the snapshot value. ' +
|
||||
'Functions may close over mutable values and cause tearing.',
|
||||
);
|
||||
}
|
||||
}
|
||||
return snapshot;
|
||||
} else {
|
||||
// This handles the special case of a mutable source being shared beween renderers.
|
||||
// In that case, if the source is mutated between the first and second renderer,
|
||||
@@ -992,6 +1001,15 @@ function useMutableSource<Source, Snapshot>(
|
||||
const maybeNewVersion = getVersion(source._source);
|
||||
if (!is(version, maybeNewVersion)) {
|
||||
const maybeNewSnapshot = getSnapshot(source._source);
|
||||
if (__DEV__) {
|
||||
if (typeof maybeNewSnapshot === 'function') {
|
||||
console.error(
|
||||
'Mutable source should not return a function as the snapshot value. ' +
|
||||
'Functions may close over mutable values and cause tearing.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!is(snapshot, maybeNewSnapshot)) {
|
||||
setSnapshot(maybeNewSnapshot);
|
||||
|
||||
|
||||
@@ -900,7 +900,16 @@ function readFromUnsubcribedMutableSource<Source, Snapshot>(
|
||||
}
|
||||
|
||||
if (isSafeToReadFromSource) {
|
||||
return getSnapshot(source._source);
|
||||
const snapshot = getSnapshot(source._source);
|
||||
if (__DEV__) {
|
||||
if (typeof snapshot === 'function') {
|
||||
console.error(
|
||||
'Mutable source should not return a function as the snapshot value. ' +
|
||||
'Functions may close over mutable values and cause tearing.',
|
||||
);
|
||||
}
|
||||
}
|
||||
return snapshot;
|
||||
} else {
|
||||
// This handles the special case of a mutable source being shared beween renderers.
|
||||
// In that case, if the source is mutated between the first and second renderer,
|
||||
@@ -978,6 +987,15 @@ function useMutableSource<Source, Snapshot>(
|
||||
const maybeNewVersion = getVersion(source._source);
|
||||
if (!is(version, maybeNewVersion)) {
|
||||
const maybeNewSnapshot = getSnapshot(source._source);
|
||||
if (__DEV__) {
|
||||
if (typeof maybeNewSnapshot === 'function') {
|
||||
console.error(
|
||||
'Mutable source should not return a function as the snapshot value. ' +
|
||||
'Functions may close over mutable values and cause tearing.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!is(snapshot, maybeNewSnapshot)) {
|
||||
setSnapshot(maybeNewSnapshot);
|
||||
|
||||
|
||||
@@ -1493,6 +1493,34 @@ describe('useMutableSource', () => {
|
||||
},
|
||||
);
|
||||
|
||||
// @gate experimental
|
||||
it('warns about functions being used as snapshot values', async () => {
|
||||
const source = createSource(() => 'a');
|
||||
const mutableSource = createMutableSource(source);
|
||||
|
||||
const getSnapshot = () => source.value;
|
||||
|
||||
function Read() {
|
||||
const fn = useMutableSource(mutableSource, getSnapshot, defaultSubscribe);
|
||||
const value = fn();
|
||||
Scheduler.unstable_yieldValue(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
const root = ReactNoop.createRoot();
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<>
|
||||
<Read />
|
||||
</>,
|
||||
);
|
||||
expect(() => expect(Scheduler).toFlushAndYield(['a'])).toErrorDev(
|
||||
'Mutable source should not return a function as the snapshot value.',
|
||||
);
|
||||
});
|
||||
expect(root).toMatchRenderedOutput('a');
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('getSnapshot changes and then source is mutated during interleaved event', async () => {
|
||||
const {useEffect} = React;
|
||||
|
||||
Reference in New Issue
Block a user