diff --git a/compat/src/index.js b/compat/src/index.js index 8d2bd16140..b3407574f3 100644 --- a/compat/src/index.js +++ b/compat/src/index.js @@ -140,8 +140,13 @@ export const useInsertionEffect = useLayoutEffect; export function useSyncExternalStore(subscribe, getSnapshot) { const [state, setState] = useState(getSnapshot); - // TODO: in suspense for data we could have a discrepancy here because Preact won't re-init the "useState" - // when this unsuspends which could lead to stale state as the subscription is torn down. + const value = getSnapshot(); + + useLayoutEffect(() => { + if (value !== state) { + setState(() => value); + } + }, [subscribe, value, getSnapshot]); useEffect(() => { return subscribe(() => { diff --git a/compat/test/browser/hooks.test.js b/compat/test/browser/hooks.test.js index cdf102c992..825cb07509 100644 --- a/compat/test/browser/hooks.test.js +++ b/compat/test/browser/hooks.test.js @@ -4,7 +4,9 @@ import React, { useInsertionEffect, useSyncExternalStore, useTransition, - render + render, + useState, + useCallback } from 'preact/compat'; import { setupRerender, act } from 'preact/test-utils'; import { setupScratch, teardown } from '../../../test/_util/helpers'; @@ -91,7 +93,7 @@ describe('React-18-hooks', () => { }); expect(scratch.innerHTML).to.equal('

hello world

'); expect(subscribe).to.be.calledOnce; - expect(getSnapshot).to.be.calledOnce; + expect(getSnapshot).to.be.calledTwice; }); it('subscribes and rerenders when called', () => { @@ -119,7 +121,7 @@ describe('React-18-hooks', () => { }); expect(scratch.innerHTML).to.equal('

hello world

'); expect(subscribe).to.be.calledOnce; - expect(getSnapshot).to.be.calledOnce; + expect(getSnapshot).to.be.calledTwice; called = true; flush(); @@ -135,9 +137,11 @@ describe('React-18-hooks', () => { return () => {}; }); + const func = () => 'value: ' + i++; + let i = 0; const getSnapshot = sinon.spy(() => { - return () => 'value: ' + i++; + return func; }); const App = () => { @@ -150,12 +154,39 @@ describe('React-18-hooks', () => { }); expect(scratch.innerHTML).to.equal('

value: 0

'); expect(subscribe).to.be.calledOnce; - expect(getSnapshot).to.be.calledOnce; + expect(getSnapshot).to.be.calledTwice; flush(); rerender(); - expect(scratch.innerHTML).to.equal('

value: 1

'); + expect(scratch.innerHTML).to.equal('

value: 0

'); + }); + + it('works with useCallback', () => { + let toggle; + const App = () => { + const [state, setState] = useState(true); + toggle = setState.bind(this, () => false); + + const value = useSyncExternalStore( + useCallback(() => { + return () => {}; + }, [state]), + () => (state ? 'yep' : 'nope') + ); + + return

{value}

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

yep

'); + + toggle(); + rerender(); + + expect(scratch.innerHTML).to.equal('

nope

'); }); }); });