@@ -1783,4 +1783,263 @@ describe('ReactDOMFizzServer', () => {
17831783 expect ( getVisibleChildren ( container ) ) . toEqual ( < div > client</ div > ) ;
17841784 expect ( ref . current ) . toEqual ( serverRenderedDiv ) ;
17851785 } ) ;
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+ ) ;
17862045} ) ;
0 commit comments