@@ -1783,4 +1783,263 @@ describe('ReactDOMFizzServer', () => {
1783
1783
expect ( getVisibleChildren ( container ) ) . toEqual ( < div > client</ div > ) ;
1784
1784
expect ( ref . current ) . toEqual ( serverRenderedDiv ) ;
1785
1785
} ) ;
1786
+
1787
+ // @gate supportsNativeUseSyncExternalStore
1788
+ // @gate experimental
1789
+ it (
1790
+ 'errors during hydration force a client render at the nearest Suspense ' +
1791
+ 'boundary, and during the client render it recovers' ,
1792
+ async ( ) => {
1793
+ let isClient = false ;
1794
+
1795
+ function subscribe ( ) {
1796
+ return ( ) => { } ;
1797
+ }
1798
+ function getClientSnapshot ( ) {
1799
+ return 'Yay!' ;
1800
+ }
1801
+
1802
+ // At the time of writing, the only API that exposes whether it's currently
1803
+ // hydrating is the `getServerSnapshot` API, so I'm using that here to
1804
+ // simulate an error during hydration.
1805
+ function getServerSnapshot ( ) {
1806
+ if ( isClient ) {
1807
+ throw new Error ( 'Hydration error' ) ;
1808
+ }
1809
+ return 'Yay!' ;
1810
+ }
1811
+
1812
+ function Child ( ) {
1813
+ const value = useSyncExternalStore (
1814
+ subscribe ,
1815
+ getClientSnapshot ,
1816
+ getServerSnapshot ,
1817
+ ) ;
1818
+ Scheduler . unstable_yieldValue ( value ) ;
1819
+ return value ;
1820
+ }
1821
+
1822
+ const span1Ref = React . createRef ( ) ;
1823
+ const span2Ref = React . createRef ( ) ;
1824
+ const span3Ref = React . createRef ( ) ;
1825
+
1826
+ function App ( ) {
1827
+ return (
1828
+ < div >
1829
+ < span ref = { span1Ref } />
1830
+ < Suspense fallback = "Loading..." >
1831
+ < span ref = { span2Ref } >
1832
+ < Child />
1833
+ </ span >
1834
+ </ Suspense >
1835
+ < span ref = { span3Ref } />
1836
+ </ div >
1837
+ ) ;
1838
+ }
1839
+
1840
+ await act ( async ( ) => {
1841
+ const { startWriting} = ReactDOMFizzServer . pipeToNodeWritable (
1842
+ < App /> ,
1843
+ writable ,
1844
+ ) ;
1845
+ startWriting ( ) ;
1846
+ } ) ;
1847
+ expect ( Scheduler ) . toHaveYielded ( [ 'Yay!' ] ) ;
1848
+
1849
+ const [ span1 , span2 , span3 ] = container . getElementsByTagName ( 'span' ) ;
1850
+
1851
+ // Hydrate the tree. Child will throw during hydration, but not when it
1852
+ // falls back to client rendering.
1853
+ isClient = true ;
1854
+ ReactDOM . hydrateRoot ( container , < App /> ) ;
1855
+
1856
+ expect ( Scheduler ) . toFlushAndYield ( [ 'Yay!' ] ) ;
1857
+ expect ( getVisibleChildren ( container ) ) . toEqual (
1858
+ < div >
1859
+ < span />
1860
+ < span > Yay!</ span >
1861
+ < span />
1862
+ </ div > ,
1863
+ ) ;
1864
+
1865
+ // The node that's inside the boundary that errored during hydration was
1866
+ // not hydrated.
1867
+ expect ( span2Ref . current ) . not . toBe ( span2 ) ;
1868
+
1869
+ // But the nodes outside the boundary were.
1870
+ expect ( span1Ref . current ) . toBe ( span1 ) ;
1871
+ expect ( span3Ref . current ) . toBe ( span3 ) ;
1872
+ } ,
1873
+ ) ;
1874
+
1875
+ // @gate experimental
1876
+ it (
1877
+ 'errors during hydration force a client render at the nearest Suspense ' +
1878
+ 'boundary, and during the client render it fails again' ,
1879
+ async ( ) => {
1880
+ // Similar to previous test, but the client render errors, too. We should
1881
+ // be able to capture it with an error boundary.
1882
+
1883
+ let isClient = false ;
1884
+
1885
+ class ErrorBoundary extends React . Component {
1886
+ state = { error : null } ;
1887
+ static getDerivedStateFromError ( error ) {
1888
+ return { error} ;
1889
+ }
1890
+ render ( ) {
1891
+ if ( this . state . error !== null ) {
1892
+ return this . state . error . message ;
1893
+ }
1894
+ return this . props . children ;
1895
+ }
1896
+ }
1897
+
1898
+ function Child ( ) {
1899
+ if ( isClient ) {
1900
+ throw new Error ( 'Oops!' ) ;
1901
+ }
1902
+ Scheduler . unstable_yieldValue ( 'Yay!' ) ;
1903
+ return 'Yay!' ;
1904
+ }
1905
+
1906
+ const span1Ref = React . createRef ( ) ;
1907
+ const span2Ref = React . createRef ( ) ;
1908
+ const span3Ref = React . createRef ( ) ;
1909
+
1910
+ function App ( ) {
1911
+ return (
1912
+ < ErrorBoundary >
1913
+ < span ref = { span1Ref } />
1914
+ < Suspense fallback = "Loading..." >
1915
+ < span ref = { span2Ref } >
1916
+ < Child />
1917
+ </ span >
1918
+ </ Suspense >
1919
+ < span ref = { span3Ref } />
1920
+ </ ErrorBoundary >
1921
+ ) ;
1922
+ }
1923
+
1924
+ await act ( async ( ) => {
1925
+ const { startWriting} = ReactDOMFizzServer . pipeToNodeWritable (
1926
+ < App /> ,
1927
+ writable ,
1928
+ ) ;
1929
+ startWriting ( ) ;
1930
+ } ) ;
1931
+ expect ( Scheduler ) . toHaveYielded ( [ 'Yay!' ] ) ;
1932
+
1933
+ // Hydrate the tree. Child will throw during render.
1934
+ isClient = true ;
1935
+ ReactDOM . hydrateRoot ( container , < App /> ) ;
1936
+
1937
+ expect ( Scheduler ) . toFlushAndYield ( [ ] ) ;
1938
+ expect ( getVisibleChildren ( container ) ) . toEqual ( 'Oops!' ) ;
1939
+ } ,
1940
+ ) ;
1941
+
1942
+ // @gate supportsNativeUseSyncExternalStore
1943
+ // @gate experimental
1944
+ it (
1945
+ 'errors during hydration force a client render at the nearest Suspense ' +
1946
+ 'boundary, and during the client render it recovers, then a deeper ' +
1947
+ 'child suspends' ,
1948
+ async ( ) => {
1949
+ let isClient = false ;
1950
+
1951
+ function subscribe ( ) {
1952
+ return ( ) => { } ;
1953
+ }
1954
+ function getClientSnapshot ( ) {
1955
+ return 'Yay!' ;
1956
+ }
1957
+
1958
+ // At the time of writing, the only API that exposes whether it's currently
1959
+ // hydrating is the `getServerSnapshot` API, so I'm using that here to
1960
+ // simulate an error during hydration.
1961
+ function getServerSnapshot ( ) {
1962
+ if ( isClient ) {
1963
+ throw new Error ( 'Hydration error' ) ;
1964
+ }
1965
+ return 'Yay!' ;
1966
+ }
1967
+
1968
+ function Child ( ) {
1969
+ const value = useSyncExternalStore (
1970
+ subscribe ,
1971
+ getClientSnapshot ,
1972
+ getServerSnapshot ,
1973
+ ) ;
1974
+ if ( isClient ) {
1975
+ readText ( value ) ;
1976
+ }
1977
+ Scheduler . unstable_yieldValue ( value ) ;
1978
+ return value ;
1979
+ }
1980
+
1981
+ const span1Ref = React . createRef ( ) ;
1982
+ const span2Ref = React . createRef ( ) ;
1983
+ const span3Ref = React . createRef ( ) ;
1984
+
1985
+ function App ( ) {
1986
+ return (
1987
+ < div >
1988
+ < span ref = { span1Ref } />
1989
+ < Suspense fallback = "Loading..." >
1990
+ < span ref = { span2Ref } >
1991
+ < Child />
1992
+ </ span >
1993
+ </ Suspense >
1994
+ < span ref = { span3Ref } />
1995
+ </ div >
1996
+ ) ;
1997
+ }
1998
+
1999
+ await act ( async ( ) => {
2000
+ const { startWriting} = ReactDOMFizzServer . pipeToNodeWritable (
2001
+ < App /> ,
2002
+ writable ,
2003
+ ) ;
2004
+ startWriting ( ) ;
2005
+ } ) ;
2006
+ expect ( Scheduler ) . toHaveYielded ( [ 'Yay!' ] ) ;
2007
+
2008
+ const [ span1 , span2 , span3 ] = container . getElementsByTagName ( 'span' ) ;
2009
+
2010
+ // Hydrate the tree. Child will throw during hydration, but not when it
2011
+ // falls back to client rendering.
2012
+ isClient = true ;
2013
+ ReactDOM . hydrateRoot ( container , < App /> ) ;
2014
+
2015
+ expect ( Scheduler ) . toFlushAndYield ( [ ] ) ;
2016
+ expect ( getVisibleChildren ( container ) ) . toEqual (
2017
+ < div >
2018
+ < span />
2019
+ Loading...
2020
+ < span />
2021
+ </ div > ,
2022
+ ) ;
2023
+
2024
+ await act ( async ( ) => {
2025
+ resolveText ( 'Yay!' ) ;
2026
+ } ) ;
2027
+ expect ( Scheduler ) . toFlushAndYield ( [ 'Yay!' ] ) ;
2028
+ expect ( getVisibleChildren ( container ) ) . toEqual (
2029
+ < div >
2030
+ < span />
2031
+ < span > Yay!</ span >
2032
+ < span />
2033
+ </ div > ,
2034
+ ) ;
2035
+
2036
+ // The node that's inside the boundary that errored during hydration was
2037
+ // not hydrated.
2038
+ expect ( span2Ref . current ) . not . toBe ( span2 ) ;
2039
+
2040
+ // But the nodes outside the boundary were.
2041
+ expect ( span1Ref . current ) . toBe ( span1 ) ;
2042
+ expect ( span3Ref . current ) . toBe ( span3 ) ;
2043
+ } ,
2044
+ ) ;
1786
2045
} ) ;
0 commit comments