@@ -844,6 +844,254 @@ describe('ProfilingCache', () => {
844844 }
845845 } ) ;
846846
847+ // @reactVersion >= 18.0
848+ it ( 'should properly detect changed hooks #2' , ( ) => {
849+ // inlined `use-sync-external-store/with-selector`
850+ function useSyncExternalStoreWithSelector < Snapshot , Selection > (
851+ subscribe : ( ( ) => void ) => ( ) => void ,
852+ getSnapshot : ( ) = > Snapshot ,
853+ getServerSnapshot : void | null | ( ( ) => Snapshot ) ,
854+ selector : ( snapshot : Snapshot ) = > Selection ,
855+ isEqual ?: ( a : Selection , b : Selection ) = > boolean ,
856+ ) : Selection {
857+ // Use this to track the rendered snapshot.
858+ const instRef = React . useRef <
859+ | {
860+ hasValue : true ,
861+ value : Selection ,
862+ }
863+ | {
864+ hasValue : false ,
865+ value : null ,
866+ }
867+ | null ,
868+ > ( null ) ;
869+ let inst ;
870+ if ( instRef . current === null ) {
871+ inst = {
872+ hasValue : false ,
873+ value : null ,
874+ } ;
875+ instRef . current = inst ;
876+ } else {
877+ inst = instRef . current ;
878+ }
879+
880+ const [ getSelection , getServerSelection ] = React . useMemo ( ( ) => {
881+ // Track the memoized state using closure variables that are local to this
882+ // memoized instance of a getSnapshot function. Intentionally not using a
883+ // useRef hook, because that state would be shared across all concurrent
884+ // copies of the hook/component.
885+ let hasMemo = false ;
886+ let memoizedSnapshot ;
887+ let memoizedSelection : Selection ;
888+ const memoizedSelector = ( nextSnapshot : Snapshot ) => {
889+ if ( ! hasMemo ) {
890+ // The first time the hook is called, there is no memoized result.
891+ hasMemo = true ;
892+ memoizedSnapshot = nextSnapshot ;
893+ const nextSelection = selector ( nextSnapshot ) ;
894+ if ( isEqual !== undefined ) {
895+ // Even if the selector has changed, the currently rendered selection
896+ // may be equal to the new selection. We should attempt to reuse the
897+ // current value if possible, to preserve downstream memoizations.
898+ if ( inst . hasValue ) {
899+ const currentSelection = inst . value ;
900+ if ( isEqual ( currentSelection , nextSelection ) ) {
901+ memoizedSelection = currentSelection ;
902+ return currentSelection ;
903+ }
904+ }
905+ }
906+ memoizedSelection = nextSelection ;
907+ return nextSelection ;
908+ }
909+
910+ // We may be able to reuse the previous invocation's result.
911+ const prevSnapshot : Snapshot = ( memoizedSnapshot : any ) ;
912+ const prevSelection : Selection = ( memoizedSelection : any ) ;
913+
914+ if ( Object . is ( prevSnapshot , nextSnapshot ) ) {
915+ // The snapshot is the same as last time. Reuse the previous selection.
916+ return prevSelection ;
917+ }
918+
919+ // The snapshot has changed, so we need to compute a new selection.
920+ const nextSelection = selector ( nextSnapshot ) ;
921+
922+ // If a custom isEqual function is provided, use that to check if the data
923+ // has changed. If it hasn't, return the previous selection. That signals
924+ // to React that the selections are conceptually equal, and we can bail
925+ // out of rendering.
926+ if ( isEqual !== undefined && isEqual ( prevSelection , nextSelection ) ) {
927+ return prevSelection ;
928+ }
929+
930+ memoizedSnapshot = nextSnapshot ;
931+ memoizedSelection = nextSelection ;
932+ return nextSelection ;
933+ } ;
934+ // Assigning this to a constant so that Flow knows it can't change.
935+ const maybeGetServerSnapshot =
936+ getServerSnapshot === undefined ? null : getServerSnapshot ;
937+ const getSnapshotWithSelector = ( ) => memoizedSelector ( getSnapshot ( ) ) ;
938+ const getServerSnapshotWithSelector =
939+ maybeGetServerSnapshot === null
940+ ? undefined
941+ : ( ) => memoizedSelector ( maybeGetServerSnapshot ( ) ) ;
942+ return [ getSnapshotWithSelector , getServerSnapshotWithSelector ] ;
943+ } , [ getSnapshot , getServerSnapshot , selector , isEqual ] ) ;
944+
945+ const value = React . useSyncExternalStore (
946+ subscribe ,
947+ getSelection ,
948+ getServerSelection ,
949+ ) ;
950+
951+ React . useEffect ( ( ) => {
952+ // $FlowFixMe[incompatible-type] changing the variant using mutation isn't supported
953+ inst . hasValue = true ;
954+ // $FlowFixMe[incompatible-type]
955+ inst . value = value ;
956+ } , [ value ] ) ;
957+
958+ React . useDebugValue ( value ) ;
959+ return value ;
960+ }
961+
962+ let reduxStore = { a : 0 , b : 0 } ;
963+ function getState ( ) {
964+ return reduxStore ;
965+ }
966+
967+ let syncExternalStoreCallback ;
968+ function subscribe ( callback ) {
969+ syncExternalStoreCallback = callback ;
970+ }
971+
972+ const ReactReduxContext = React . createContext ( 0 ) ;
973+ const DropTargetContext = React . createContext ( 0 ) ;
974+ const EditorContext = React . createContext ( 0 ) ;
975+
976+ const ResizableComponent = React . memo ( function ResizableComponent ( props ) {
977+ // 0
978+ React . useRef ( null ) ;
979+ React . useContext ( EditorContext ) ;
980+ React . useContext ( DropTargetContext ) ;
981+ // useShowPropertyPane
982+ // useDispatch
983+ React . useContext ( ReactReduxContext ) ;
984+ // 1
985+ React . useCallback ( ( ) => { } , [ ] ) ;
986+ // useWidgetSelection
987+ // useDispatch
988+ React . useContext ( ReactReduxContext ) ;
989+ // 2
990+ React . useCallback ( ( ) => { } , [ ] ) ;
991+ // 3
992+ React . useCallback ( ( ) => { } , [ ] ) ;
993+ // useWidgetDragResize
994+ // useDispatch
995+ React . useContext ( ReactReduxContext ) ;
996+ // 4
997+ React . useCallback ( ( ) => { } , [ ] ) ;
998+ // 5
999+ React . useCallback ( ( ) => { } , [ ] ) ;
1000+ // useSelector
1001+ React . useContext ( ReactReduxContext ) ;
1002+ // 6
1003+ React . useRef ( ) ;
1004+ // 7
1005+ React . useCallback ( ) ;
1006+ // 8,9,10,11
1007+ const selectedState1 = useSyncExternalStoreWithSelector (
1008+ subscribe ,
1009+ getState ,
1010+ getState ,
1011+ state => state . a ,
1012+ ) ;
1013+ React . useDebugValue ( selectedState1 ) ;
1014+ // useSelector
1015+ React . useContext ( ReactReduxContext ) ;
1016+ // 12
1017+ React . useRef ( ) ;
1018+ // 13
1019+ React . useCallback ( ) ;
1020+ // 14,15,16,17
1021+ const selectedState2 = useSyncExternalStoreWithSelector (
1022+ subscribe ,
1023+ getState ,
1024+ getState ,
1025+ state => state . a ,
1026+ ) ;
1027+ React . useDebugValue ( selectedState2 ) ;
1028+ // useSelector
1029+ React . useContext ( ReactReduxContext ) ;
1030+ // 18
1031+ React . useRef ( ) ;
1032+ // 19
1033+ React . useCallback ( ) ;
1034+ // 20,21,22,23
1035+ const selectedState3 = useSyncExternalStoreWithSelector (
1036+ subscribe ,
1037+ getState ,
1038+ getState ,
1039+ state => state . a ,
1040+ ) ;
1041+ React . useDebugValue ( selectedState3 ) ;
1042+ // useSelector
1043+ React . useContext ( ReactReduxContext ) ;
1044+ // 24
1045+ React . useRef ( ) ;
1046+ // 25
1047+ React . useCallback ( ) ;
1048+ // 26,27,28,29
1049+ const valueThatChanges = useSyncExternalStoreWithSelector (
1050+ subscribe ,
1051+ getState ,
1052+ getState ,
1053+ state => state . b ,
1054+ ) ;
1055+ React . useDebugValue ( valueThatChanges ) ;
1056+
1057+ return valueThatChanges ;
1058+ } ) ;
1059+
1060+ utils . act ( ( ) => render ( < ResizableComponent /> ) ) ;
1061+
1062+ utils . act ( ( ) => store . profilerStore . startProfiling ( ) ) ;
1063+
1064+ utils . act ( ( ) => {
1065+ reduxStore = { ...reduxStore , b : reduxStore . b + 1 } ;
1066+ syncExternalStoreCallback ( ) ;
1067+ } ) ;
1068+
1069+ utils . act ( ( ) => store . profilerStore . stopProfiling ( ) ) ;
1070+
1071+ const rootID = store . roots [ 0 ] ;
1072+
1073+ const changeDescriptions = store . profilerStore
1074+ . getDataForRoot ( rootID )
1075+ . commitData . map ( commitData => commitData . changeDescriptions ) ;
1076+
1077+ expect ( changeDescriptions ) . toMatchInlineSnapshot ( `
1078+ [
1079+ Map {
1080+ 2 => {
1081+ "context": false,
1082+ "didHooksChange": true,
1083+ "hooks": [
1084+ 31,
1085+ ],
1086+ "isFirstMount": false,
1087+ "props": [],
1088+ "state": null,
1089+ },
1090+ },
1091+ ]
1092+ ` ) ;
1093+ } ) ;
1094+
8471095 // @reactVersion >= 18.0
8481096 it ( 'should calculate durations based on actual children (not filtered children)' , ( ) => {
8491097 store . componentFilters = [ utils . createDisplayNameFilter ( '^Parent$' ) ] ;
0 commit comments