31
32
function Text(props) {
33
Scheduler.unstable_yieldValue(props.text);
34
- return <span prop={props.text} />;
34
+ return <span prop={props.text}>{props.children}</span>;
35
+ }
36
+
37
+ function LoggedText({text, children}) {
38
+ useEffect(() => {
39
+ Scheduler.unstable_yieldValue(`mount ${text}`);
40
+ return () => {
41
+ Scheduler.unstable_yieldValue(`unmount ${text}`);
42
+ };
43
+ });
44
+
45
+ useLayoutEffect(() => {
46
+ Scheduler.unstable_yieldValue(`mount layout ${text}`);
47
+ return () => {
48
+ Scheduler.unstable_yieldValue(`unmount layout ${text}`);
49
+ };
50
+ });
51
+ return <Text text={text}>{children}</Text>;
52
}
53
54
// @gate enableLegacyHidden
1537
);
1538
1539
expect(offscreenRef.current).not.toBeNull();
1523
- expect(offscreenRef.current.detach).not.toBeNull();
1540
1541
// Offscreen is attached by default. State updates from offscreen are **not defered**.
1542
await act(async () => {
1554
);
1555
});
1556
1541
- // detaching offscreen.
1542
- offscreenRef.current.detach();
1557
+ await act(async () => {
1558
+ offscreenRef.current.detach();
1559
+ });
1560
1561
// Offscreen is detached. State updates from offscreen are **defered**.
1562
await act(async () => {
1578
<span prop="Child 2" />
1579
</>,
1580
);
1581
+
1582
+ await act(async () => {
1583
+ offscreenRef.current.attach();
1584
+ });
1585
+
1586
+ // Offscreen is attached. State updates from offscreen are **not defered**.
1587
+ await act(async () => {
1588
+ updateChildState(3);
1589
+ updateHighPriorityComponentState(3);
1590
+ expect(Scheduler).toFlushUntilNextPaint([
1591
+ 'HighPriorityComponent 3',
1592
+ 'Child 3',
1593
+ ]);
1594
+ expect(root).toMatchRenderedOutput(
1595
+ <>
1596
+ <span prop="HighPriorityComponent 3" />
1597
+ <span prop="Child 3" />
1598
+ </>,
1599
+ );
1600
+ });
1601
});
1602
1603
// @gate enableOffscreen
1606
let updateHighPriorityComponentState;
1607
let offscreenRef;
1608
let nextRenderTriggerDetach = false;
1609
+ let nextRenderTriggerAttach = false;
1610
1611
function Child() {
1612
const [state, _stateUpdate] = useState(0);
1621
const text = 'HighPriorityComponent ' + state;
1622
useLayoutEffect(() => {
1623
if (nextRenderTriggerDetach) {
1586
- offscreenRef.current.detach();
1624
_stateUpdate(state + 1);
1625
updateChildState(state + 1);
1626
+ offscreenRef.current.detach();
1627
nextRenderTriggerDetach = false;
1628
}
1629
+
1630
+ if (nextRenderTriggerAttach) {
1631
+ offscreenRef.current.attach();
1632
+ nextRenderTriggerAttach = false;
1633
+ }
1634
});
1635
return (
1636
<>
1663
1664
nextRenderTriggerDetach = true;
1665
1623
- // Offscreen is attached. State updates from offscreen are **not defered**.
1624
- // Offscreen is detached inside useLayoutEffect;
1666
+ // Offscreen is attached and gets detached inside useLayoutEffect.
1667
+ // State updates from offscreen are **defered**.
1668
await act(async () => {
1669
updateChildState(1);
1670
updateHighPriorityComponentState(1);
1672
'HighPriorityComponent 1',
1673
'Child 1',
1674
'HighPriorityComponent 2',
1632
- 'Child 2',
1675
]);
1676
expect(root).toMatchRenderedOutput(
1677
<>
1678
<span prop="HighPriorityComponent 2" />
1637
- <span prop="Child 2" />
1679
+ <span prop="Child 1" />
1680
</>,
1681
);
1682
});
1683
1684
+ expect(Scheduler).toHaveYielded(['Child 2']);
1685
+ expect(root).toMatchRenderedOutput(
1686
+ <>
1687
+ <span prop="HighPriorityComponent 2" />
1688
+ <span prop="Child 2" />
1689
+ </>,
1690
+ );
1691
+
1692
+ nextRenderTriggerAttach = true;
1693
+
1694
// Offscreen is detached. State updates from offscreen are **defered**.
1695
+ // Offscreen is attached inside useLayoutEffect;
1696
await act(async () => {
1697
updateChildState(3);
1698
updateHighPriorityComponentState(3);
1646
- expect(Scheduler).toFlushUntilNextPaint(['HighPriorityComponent 3']);
1699
+ expect(Scheduler).toFlushUntilNextPaint([
1700
+ 'HighPriorityComponent 3',
1701
+ 'Child 3',
1702
+ ]);
1703
expect(root).toMatchRenderedOutput(
1704
<>
1705
<span prop="HighPriorityComponent 3" />
1650
- <span prop="Child 2" />
1706
+ <span prop="Child 3" />
1707
</>,
1708
);
1709
});
1654
-
1655
- expect(Scheduler).toHaveYielded(['Child 3']);
1656
- expect(root).toMatchRenderedOutput(
1657
- <>
1658
- <span prop="HighPriorityComponent 3" />
1659
- <span prop="Child 3" />
1660
- </>,
1661
- );
1710
});
1711
});
1712
1819
expect(offscreenRef.current._current === firstFiber).toBeFalsy();
1820
});
1821
1774
- // TODO: When attach/detach methods are implemented. Add tests for nested Offscreen case.
1822
+ // @gate enableOffscreen
1823
+ it('does not mount tree until attach is called', async () => {
1824
+ let offscreenRef;
1825
+ let spanRef;
1826
+
1827
+ function Child() {
1828
+ spanRef = useRef(null);
1829
+ useEffect(() => {
1830
+ Scheduler.unstable_yieldValue('Mount Child');
1831
+ return () => {
1832
+ Scheduler.unstable_yieldValue('Unmount Child');
1833
+ };
1834
+ });
1835
+ useLayoutEffect(() => {
1836
+ Scheduler.unstable_yieldValue('Mount Layout Child');
1837
+ return () => {
1838
+ Scheduler.unstable_yieldValue('Unmount Layout Child');
1839
+ };
1840
+ });
1841
+
1842
+ return <span ref={spanRef}>Child</span>;
1843
+ }
1844
+
1845
+ function App() {
1846
+ return (
1847
+ <Offscreen mode={'manual'} ref={el => (offscreenRef = el)}>
1848
+ <Child />
1849
+ </Offscreen>
1850
+ );
1851
+ }
1852
+
1853
+ const root = ReactNoop.createRoot();
1854
+
1855
+ await act(async () => {
1856
+ root.render(<App />);
1857
+ });
1858
+
1859
+ expect(offscreenRef).not.toBeNull();
1860
+ expect(spanRef.current).not.toBeNull();
1861
+ expect(Scheduler).toHaveYielded(['Mount Layout Child', 'Mount Child']);
1862
+
1863
+ await act(async () => {
1864
+ offscreenRef.detach();
1865
+ });
1866
+
1867
+ expect(spanRef.current).toBeNull();
1868
+ expect(Scheduler).toHaveYielded(['Unmount Layout Child', 'Unmount Child']);
1869
+
1870
+ // Calling attach on already attached Offscreen.
1871
+ await act(async () => {
1872
+ offscreenRef.detach();
1873
+ });
1874
+
1875
+ expect(Scheduler).toHaveYielded([]);
1876
+
1877
+ await act(async () => {
1878
+ offscreenRef.attach();
1879
+ });
1880
+
1881
+ expect(spanRef.current).not.toBeNull();
1882
+ expect(Scheduler).toHaveYielded(['Mount Layout Child', 'Mount Child']);
1883
+
1884
+ // Calling attach on already attached Offscreen
1885
+ offscreenRef.attach();
1886
+
1887
+ expect(Scheduler).toHaveYielded([]);
1888
+ });
1889
+
1890
+ // @gate enableOffscreen
1891
+ it('handles nested manual offscreens', async () => {
1892
+ let outerOffscreen;
1893
+ let innerOffscreen;
1894
+
1895
+ function App() {
1896
+ return (
1897
+ <LoggedText text={'outer'}>
1898
+ <Offscreen mode={'manual'} ref={el => (outerOffscreen = el)}>
1899
+ <LoggedText text={'middle'}>
1900
+ <Offscreen mode={'manual'} ref={el => (innerOffscreen = el)}>
1901
+ <LoggedText text={'inner'} />
1902
+ </Offscreen>
1903
+ </LoggedText>
1904
+ </Offscreen>
1905
+ </LoggedText>
1906
+ );
1907
+ }
1908
+
1909
+ const root = ReactNoop.createRoot();
1910
+
1911
+ await act(async () => {
1912
+ root.render(<App />);
1913
+ });
1914
+
1915
+ expect(Scheduler).toHaveYielded([
1916
+ 'outer',
1917
+ 'middle',
1918
+ 'inner',
1919
+ 'mount layout inner',
1920
+ 'mount layout middle',
1921
+ 'mount layout outer',
1922
+ 'mount inner',
1923
+ 'mount middle',
1924
+ 'mount outer',
1925
+ ]);
1926
+
1927
+ expect(outerOffscreen).not.toBeNull();
1928
+ expect(innerOffscreen).not.toBeNull();
1929
+
1930
+ await act(async () => {
1931
+ outerOffscreen.detach();
1932
+ });
1933
+
1934
+ expect(innerOffscreen).toBeNull();
1935
+
1936
+ expect(Scheduler).toHaveYielded([
1937
+ 'unmount layout middle',
1938
+ 'unmount layout inner',
1939
+ 'unmount middle',
1940
+ 'unmount inner',
1941
+ ]);
1942
+
1943
+ await act(async () => {
1944
+ outerOffscreen.attach();
1945
+ });
1946
+
1947
+ expect(Scheduler).toHaveYielded([
1948
+ 'mount layout inner',
1949
+ 'mount layout middle',
1950
+ 'mount inner',
1951
+ 'mount middle',
1952
+ ]);
1953
+
1954
+ await act(async () => {
1955
+ innerOffscreen.detach();
1956
+ });
1957
+
1958
+ expect(Scheduler).toHaveYielded(['unmount layout inner', 'unmount inner']);
1959
+
1960
+ // Calling detach on already detached Offscreen.
1961
+ await act(async () => {
1962
+ innerOffscreen.detach();
1963
+ });
1964
+
1965
+ expect(Scheduler).toHaveYielded([]);
1966
+
1967
+ await act(async () => {
1968
+ innerOffscreen.attach();
1969
+ });
1970
+
1971
+ expect(Scheduler).toHaveYielded(['mount layout inner', 'mount inner']);
1972
+
1973
+ await act(async () => {
1974
+ innerOffscreen.detach();
1975
+ outerOffscreen.attach();
1976
+ });
1977
+
1978
+ expect(Scheduler).toHaveYielded(['unmount layout inner', 'unmount inner']);
1979
+ });
1980
+
1981
+ // @gate enableOffscreen
1982
+ it('batches multiple attach and detach calls scheduled from an event handler', async () => {
1983
+ function Child() {
1984
+ useEffect(() => {
1985
+ Scheduler.unstable_yieldValue('attach child');
1986
+ return () => {
1987
+ Scheduler.unstable_yieldValue('detach child');
1988
+ };
1989
+ }, []);
1990
+ return 'child';
1991
+ }
1992
+
1993
+ const offscreen = React.createRef(null);
1994
+ function App() {
1995
+ return (
1996
+ <Offscreen ref={offscreen} mode="manual">
1997
+ <Child />
1998
+ </Offscreen>
1999
+ );
2000
+ }
2001
+
2002
+ const root = ReactNoop.createRoot();
2003
+ await act(() => {
2004
+ root.render(<App />);
2005
+ });
2006
+
2007
+ expect(Scheduler).toHaveYielded(['attach child']);
2008
+
2009
+ await act(async () => {
2010
+ const instance = offscreen.current;
2011
+ // Detach then immediately attach the instance.
2012
+ instance.detach();
2013
+ instance.attach();
2014
+ });
2015
+
2016
+ expect(Scheduler).toHaveYielded([]);
2017
+
2018
+ await act(async () => {
2019
+ const instance = offscreen.current;
2020
+ instance.detach();
2021
+ });
2022
+
2023
+ expect(Scheduler).toHaveYielded(['detach child']);
2024
+
2025
+ await act(async () => {
2026
+ const instance = offscreen.current;
2027
+ // Attach then immediately detach.
2028
+ instance.attach();
2029
+ instance.detach();
2030
+ });
2031
+
2032
+ expect(Scheduler).toHaveYielded([]);
2033
+ });
2034
+
2035
+ // @gate enableOffscreen
2036
+ it('batches multiple attach and detach calls scheduled from an effect', async () => {
2037
+ function Child() {
2038
+ useEffect(() => {
2039
+ Scheduler.unstable_yieldValue('attach child');
2040
+ return () => {
2041
+ Scheduler.unstable_yieldValue('detach child');
2042
+ };
2043
+ }, []);
2044
+ return 'child';
2045
+ }
2046
+
2047
+ function App() {
2048
+ const offscreen = useRef(null);
2049
+ useLayoutEffect(() => {
2050
+ const instance = offscreen.current;
2051
+ // Detach then immediately attach the instance.
2052
+ instance.detach();
2053
+ instance.attach();
2054
+ }, []);
2055
+ return (
2056
+ <Offscreen ref={offscreen} mode="manual">
2057
+ <Child />
2058
+ </Offscreen>
2059
+ );
2060
+ }
2061
+
2062
+ const root = ReactNoop.createRoot();
2063
+ await act(() => {
2064
+ root.render(<App />);
2065
+ });
2066
+ expect(Scheduler).toHaveYielded(['attach child']);
2067
+ });
2068
});