diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 8f87aea6cf91c..7c24cfa2b3b90 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -914,7 +914,16 @@ function readFromUnsubcribedMutableSource( } 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( 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); diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index 6b733ac6986c5..fbb9a55b27918 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -900,7 +900,16 @@ function readFromUnsubcribedMutableSource( } 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( 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); diff --git a/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js b/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js index 7bdb1b44207a5..4523e753beb3d 100644 --- a/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js +++ b/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js @@ -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( + <> + + , + ); + 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;