From 528a7760c1cdec7515d514c71875860528e6dc12 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 24 Oct 2022 18:10:44 +0300 Subject: [PATCH 1/2] Use Object.is in useSyncExternalStore --- compat/src/index.js | 7 ++++--- compat/src/util.js | 10 ++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/compat/src/index.js b/compat/src/index.js index 6b9988951c..c9686074f4 100644 --- a/compat/src/index.js +++ b/compat/src/index.js @@ -27,6 +27,7 @@ import { Children } from './Children'; import { Suspense, lazy } from './suspense'; import { SuspenseList } from './suspense-list'; import { createPortal } from './portals'; +import { is } from './util'; import { hydrate, render, @@ -149,18 +150,18 @@ export function useSyncExternalStore(subscribe, getSnapshot) { _instance._value = value; _instance._getSnapshot = getSnapshot; - if (_instance._value !== getSnapshot()) { + if (!is(_instance._value, getSnapshot())) { forceUpdate({ _instance }); } }, [subscribe, value, getSnapshot]); useEffect(() => { - if (_instance._value !== _instance._getSnapshot()) { + if (!is(_instance._value, _instance._getSnapshot())) { forceUpdate({ _instance }); } return subscribe(() => { - if (_instance._value !== _instance._getSnapshot()) { + if (!is(_instance._value, _instance._getSnapshot())) { forceUpdate({ _instance }); } }); diff --git a/compat/src/util.js b/compat/src/util.js index fa12b09876..a689c0f4fc 100644 --- a/compat/src/util.js +++ b/compat/src/util.js @@ -26,3 +26,13 @@ export function removeNode(node) { let parentNode = node.parentNode; if (parentNode) parentNode.removeChild(node); } + +/** + * Check if two values are the same value + * @param {*} x + * @param {*} y + * @returns {boolean} + */ +export function is(x, y) { + return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y); +} From 9f7a922e33ca391edc0e84d27270e846636dd51c Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 24 Oct 2022 23:13:16 +0300 Subject: [PATCH 2/2] Add useSyncExternalStore NaN infinite loop test --- compat/test/browser/hooks.test.js | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/compat/test/browser/hooks.test.js b/compat/test/browser/hooks.test.js index 3255704756..1a4b85a456 100644 --- a/compat/test/browser/hooks.test.js +++ b/compat/test/browser/hooks.test.js @@ -130,6 +130,40 @@ describe('React-18-hooks', () => { expect(scratch.innerHTML).to.equal('

hello new world

'); }); + it('getSnapshot can return NaN without causing infinite loop', () => { + let flush; + const subscribe = sinon.spy(cb => { + flush = cb; + return () => {}; + }); + let called = false; + const getSnapshot = sinon.spy(() => { + if (called) { + return NaN; + } + + return 1; + }); + + const App = () => { + const value = useSyncExternalStore(subscribe, getSnapshot); + return

{value}

; + }; + + act(() => { + render(, scratch); + }); + expect(scratch.innerHTML).to.equal('

1

'); + expect(subscribe).to.be.calledOnce; + expect(getSnapshot).to.be.calledThrice; + + called = true; + flush(); + rerender(); + + expect(scratch.innerHTML).to.equal('

NaN

'); + }); + it('should not call function values on subscription', () => { let flush; const subscribe = sinon.spy(cb => {