From 88bf973b4692a393559f919c0c1d208b61d990f8 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Mon, 8 Aug 2022 15:47:10 +0200 Subject: [PATCH 1/3] fix for apollo relying on the state changing between renders --- compat/src/index.js | 9 +++++++-- compat/test/browser/hooks.test.js | 31 ++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/compat/src/index.js b/compat/src/index.js index a7a31a04f5..7de04c7d63 100644 --- a/compat/src/index.js +++ b/compat/src/index.js @@ -136,8 +136,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..90dcc5cc34 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'; @@ -157,5 +159,32 @@ describe('React-18-hooks', () => { expect(scratch.innerHTML).to.equal('

value: 1

'); }); + + it.only('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

'); + }); }); }); From cb5004089cee3c6f000e257bfc14355c23df853e Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Mon, 8 Aug 2022 15:48:44 +0200 Subject: [PATCH 2/3] remove .only --- compat/test/browser/hooks.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compat/test/browser/hooks.test.js b/compat/test/browser/hooks.test.js index 90dcc5cc34..aba4455a3a 100644 --- a/compat/test/browser/hooks.test.js +++ b/compat/test/browser/hooks.test.js @@ -160,7 +160,7 @@ describe('React-18-hooks', () => { expect(scratch.innerHTML).to.equal('

value: 1

'); }); - it.only('works with useCallback', () => { + it('works with useCallback', () => { let toggle; const App = () => { const [state, setState] = useState(true); From b1d3ac3996c4ed360ce2724e82ec99be8f0d584b Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Mon, 8 Aug 2022 16:02:01 +0200 Subject: [PATCH 3/3] use sync external store requires stable getSnapshot results --- compat/src/index.js | 2 +- compat/test/browser/hooks.test.js | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/compat/src/index.js b/compat/src/index.js index 7de04c7d63..b7312f00d3 100644 --- a/compat/src/index.js +++ b/compat/src/index.js @@ -140,7 +140,7 @@ export function useSyncExternalStore(subscribe, getSnapshot) { useLayoutEffect(() => { if (value !== state) { - setState(value); + setState(() => value); } }, [subscribe, value, getSnapshot]); diff --git a/compat/test/browser/hooks.test.js b/compat/test/browser/hooks.test.js index aba4455a3a..825cb07509 100644 --- a/compat/test/browser/hooks.test.js +++ b/compat/test/browser/hooks.test.js @@ -93,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', () => { @@ -121,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(); @@ -137,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 = () => { @@ -152,12 +154,12 @@ 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', () => {