@@ -1783,4 +1783,262 @@ 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
+ it (
1876
+ 'errors during hydration force a client render at the nearest Suspense ' +
1877
+ 'boundary, and during the client render it fails again' ,
1878
+ async ( ) => {
1879
+ // Similar to previous test, but the client render errors, too. We should
1880
+ // be able to capture it with an error boundary.
1881
+
1882
+ let isClient = false ;
1883
+
1884
+ class ErrorBoundary extends React . Component {
1885
+ state = { error : null } ;
1886
+ static getDerivedStateFromError ( error ) {
1887
+ return { error} ;
1888
+ }
1889
+ render ( ) {
1890
+ if ( this . state . error !== null ) {
1891
+ return this . state . error . message ;
1892
+ }
1893
+ return this . props . children ;
1894
+ }
1895
+ }
1896
+
1897
+ function Child ( ) {
1898
+ if ( isClient ) {
1899
+ throw new Error ( 'Oops!' ) ;
1900
+ }
1901
+ Scheduler . unstable_yieldValue ( 'Yay!' ) ;
1902
+ return 'Yay!' ;
1903
+ }
1904
+
1905
+ const span1Ref = React . createRef ( ) ;
1906
+ const span2Ref = React . createRef ( ) ;
1907
+ const span3Ref = React . createRef ( ) ;
1908
+
1909
+ function App ( ) {
1910
+ return (
1911
+ < ErrorBoundary >
1912
+ < span ref = { span1Ref } />
1913
+ < Suspense fallback = "Loading..." >
1914
+ < span ref = { span2Ref } >
1915
+ < Child />
1916
+ </ span >
1917
+ </ Suspense >
1918
+ < span ref = { span3Ref } />
1919
+ </ ErrorBoundary >
1920
+ ) ;
1921
+ }
1922
+
1923
+ await act ( async ( ) => {
1924
+ const { startWriting} = ReactDOMFizzServer . pipeToNodeWritable (
1925
+ < App /> ,
1926
+ writable ,
1927
+ ) ;
1928
+ startWriting ( ) ;
1929
+ } ) ;
1930
+ expect ( Scheduler ) . toHaveYielded ( [ 'Yay!' ] ) ;
1931
+
1932
+ // Hydrate the tree. Child will throw during render.
1933
+ isClient = true ;
1934
+ ReactDOM . hydrateRoot ( container , < App /> ) ;
1935
+
1936
+ expect ( Scheduler ) . toFlushAndYield ( [ ] ) ;
1937
+ expect ( getVisibleChildren ( container ) ) . toEqual ( 'Oops!' ) ;
1938
+ } ,
1939
+ ) ;
1940
+
1941
+ // @gate supportsNativeUseSyncExternalStore
1942
+ // @gate experimental
1943
+ it (
1944
+ 'errors during hydration force a client render at the nearest Suspense ' +
1945
+ 'boundary, and during the client render it recovers, then a deeper ' +
1946
+ 'child suspends' ,
1947
+ async ( ) => {
1948
+ let isClient = false ;
1949
+
1950
+ function subscribe ( ) {
1951
+ return ( ) => { } ;
1952
+ }
1953
+ function getClientSnapshot ( ) {
1954
+ return 'Yay!' ;
1955
+ }
1956
+
1957
+ // At the time of writing, the only API that exposes whether it's currently
1958
+ // hydrating is the `getServerSnapshot` API, so I'm using that here to
1959
+ // simulate an error during hydration.
1960
+ function getServerSnapshot ( ) {
1961
+ if ( isClient ) {
1962
+ throw new Error ( 'Hydration error' ) ;
1963
+ }
1964
+ return 'Yay!' ;
1965
+ }
1966
+
1967
+ function Child ( ) {
1968
+ const value = useSyncExternalStore (
1969
+ subscribe ,
1970
+ getClientSnapshot ,
1971
+ getServerSnapshot ,
1972
+ ) ;
1973
+ if ( isClient ) {
1974
+ readText ( value ) ;
1975
+ }
1976
+ Scheduler . unstable_yieldValue ( value ) ;
1977
+ return value ;
1978
+ }
1979
+
1980
+ const span1Ref = React . createRef ( ) ;
1981
+ const span2Ref = React . createRef ( ) ;
1982
+ const span3Ref = React . createRef ( ) ;
1983
+
1984
+ function App ( ) {
1985
+ return (
1986
+ < div >
1987
+ < span ref = { span1Ref } />
1988
+ < Suspense fallback = "Loading..." >
1989
+ < span ref = { span2Ref } >
1990
+ < Child />
1991
+ </ span >
1992
+ </ Suspense >
1993
+ < span ref = { span3Ref } />
1994
+ </ div >
1995
+ ) ;
1996
+ }
1997
+
1998
+ await act ( async ( ) => {
1999
+ const { startWriting} = ReactDOMFizzServer . pipeToNodeWritable (
2000
+ < App /> ,
2001
+ writable ,
2002
+ ) ;
2003
+ startWriting ( ) ;
2004
+ } ) ;
2005
+ expect ( Scheduler ) . toHaveYielded ( [ 'Yay!' ] ) ;
2006
+
2007
+ const [ span1 , span2 , span3 ] = container . getElementsByTagName ( 'span' ) ;
2008
+
2009
+ // Hydrate the tree. Child will throw during hydration, but not when it
2010
+ // falls back to client rendering.
2011
+ isClient = true ;
2012
+ ReactDOM . hydrateRoot ( container , < App /> ) ;
2013
+
2014
+ expect ( Scheduler ) . toFlushAndYield ( [ ] ) ;
2015
+ expect ( getVisibleChildren ( container ) ) . toEqual (
2016
+ < div >
2017
+ < span />
2018
+ Loading...
2019
+ < span />
2020
+ </ div > ,
2021
+ ) ;
2022
+
2023
+ await act ( async ( ) => {
2024
+ resolveText ( 'Yay!' ) ;
2025
+ } ) ;
2026
+ expect ( Scheduler ) . toFlushAndYield ( [ 'Yay!' ] ) ;
2027
+ expect ( getVisibleChildren ( container ) ) . toEqual (
2028
+ < div >
2029
+ < span />
2030
+ < span > Yay!</ span >
2031
+ < span />
2032
+ </ div > ,
2033
+ ) ;
2034
+
2035
+ // The node that's inside the boundary that errored during hydration was
2036
+ // not hydrated.
2037
+ expect ( span2Ref . current ) . not . toBe ( span2 ) ;
2038
+
2039
+ // But the nodes outside the boundary were.
2040
+ expect ( span1Ref . current ) . toBe ( span1 ) ;
2041
+ expect ( span3Ref . current ) . toBe ( span3 ) ;
2042
+ } ,
2043
+ ) ;
1786
2044
} ) ;
0 commit comments