Add LegacyHidden to server renderer (#18919)
* Add LegacyHidden to server renderer When the tree is hidden, the server renderer renders nothing. The contents will be completely client rendered. When the tree is visible it acts like a fragment. The future streaming server renderer may want to pre-render these trees and send them down in chunks, as with Suspense boundaries. * Force client render, even at Offscreen pri
Andrew Clark committed
May 14, 2020 at 11:28 UTC
0fb747f368635933f27d296be96c8edcf99cbdbc
3 files changed
+110
-1
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+89
@@ -2809,4 +2809,93 @@ describe('ReactDOMServerPartialHydration', () => {
2809
// Now we're hydrated.
2810
expect(ref.current).not.toBe(null);
2811
});
2812
+
2813
+ // @gate experimental
2814
+ // @gate new
2815
+ it('renders a hidden LegacyHidden component', async () => {
2816
+ const LegacyHidden = React.unstable_LegacyHidden;
2817
+
2818
+ const ref = React.createRef();
2819
+
2820
+ function App() {
2821
+ return (
2822
+ <LegacyHidden mode="hidden">
2823
+ <span ref={ref}>Hidden child</span>
2824
+ </LegacyHidden>
2825
+ );
2826
+ }
2827
+
2828
+ const finalHTML = ReactDOMServer.renderToString(<App />);
2829
+
2830
+ const container = document.createElement('div');
2831
+ container.innerHTML = finalHTML;
2832
+
2833
+ const span = container.getElementsByTagName('span')[0];
2834
+ expect(span).toBe(undefined);
2835
+
2836
+ const root = ReactDOM.createRoot(container, {hydrate: true});
2837
+ root.render(<App />);
2838
+ Scheduler.unstable_flushAll();
2839
+ expect(ref.current.innerHTML).toBe('Hidden child');
2840
+ });
2841
+
2842
+ // @gate experimental
2843
+ // @gate new
2844
+ it('renders a hidden LegacyHidden component inside a Suspense boundary', async () => {
2845
+ const LegacyHidden = React.unstable_LegacyHidden;
2846
+
2847
+ const ref = React.createRef();
2848
+
2849
+ function App() {
2850
+ return (
2851
+ <Suspense fallback="Loading...">
2852
+ <LegacyHidden mode="hidden">
2853
+ <span ref={ref}>Hidden child</span>
2854
+ </LegacyHidden>
2855
+ </Suspense>
2856
+ );
2857
+ }
2858
+
2859
+ const finalHTML = ReactDOMServer.renderToString(<App />);
2860
+
2861
+ const container = document.createElement('div');
2862
+ container.innerHTML = finalHTML;
2863
+
2864
+ const span = container.getElementsByTagName('span')[0];
2865
+ expect(span).toBe(undefined);
2866
+
2867
+ const root = ReactDOM.createRoot(container, {hydrate: true});
2868
+ root.render(<App />);
2869
+ Scheduler.unstable_flushAll();
2870
+ expect(ref.current.innerHTML).toBe('Hidden child');
2871
+ });
2872
+
2873
+ // @gate experimental
2874
+ // @gate new
2875
+ it('renders a visible LegacyHidden component', async () => {
2876
+ const LegacyHidden = React.unstable_LegacyHidden;
2877
+
2878
+ const ref = React.createRef();
2879
+
2880
+ function App() {
2881
+ return (
2882
+ <LegacyHidden mode="visible">
2883
+ <span ref={ref}>Hidden child</span>
2884
+ </LegacyHidden>
2885
+ );
2886
+ }
2887
+
2888
+ const finalHTML = ReactDOMServer.renderToString(<App />);
2889
+
2890
+ const container = document.createElement('div');
2891
+ container.innerHTML = finalHTML;
2892
+
2893
+ const span = container.getElementsByTagName('span')[0];
2894
+
2895
+ const root = ReactDOM.createRoot(container, {hydrate: true});
2896
+ root.render(<App />);
2897
+ Scheduler.unstable_flushAll();
2898
+ expect(ref.current).toBe(span);
2899
+ expect(ref.current.innerHTML).toBe('Hidden child');
2900
+ });
2901
});
packages/react-dom/src/server/ReactPartialRenderer.js
+13
@@ -42,6 +42,7 @@ import {
42
REACT_MEMO_TYPE,
43
REACT_FUNDAMENTAL_TYPE,
44
REACT_SCOPE_TYPE,
45
+ REACT_LEGACY_HIDDEN_TYPE,
46
} from 'shared/ReactSymbols';
47
48
import {
@@ -1019,6 +1020,18 @@ class ReactDOMServerRenderer {
1020
}
1021
1022
switch (elementType) {
1023
+ case REACT_LEGACY_HIDDEN_TYPE: {
1024
+ if (!enableSuspenseServerRenderer) {
1025
+ break;
1026
+ }
1027
+ if (((nextChild: any): ReactElement).props.mode === 'hidden') {
1028
+ // In hidden mode, render nothing.
1029
+ return '';
1030
+ }
1031
+ // Otherwise the tree is visible, so act like a fragment.
1032
+ }
1033
+ // Intentional fall through
1034
+ // eslint-disable-next-line no-fallthrough
1035
case REACT_DEBUG_TRACING_MODE_TYPE:
1036
case REACT_STRICT_MODE_TYPE:
1037
case REACT_PROFILER_TYPE:
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+8
-1
@@ -165,6 +165,7 @@ import {
165
reenterHydrationStateFromDehydratedSuspenseInstance,
166
resetHydrationState,
167
tryToClaimNextHydratableInstance,
168
+ getIsHydrating,
169
warnIfHydrating,
170
} from './ReactFiberHydrationContext.new';
171
import {
@@ -574,7 +575,13 @@ function updateOffscreenComponent(
575
};
576
workInProgress.memoizedState = nextState;
577
pushRenderLanes(workInProgress, renderLanes);
577
- } else if (!includesSomeLane(renderLanes, (OffscreenLane: Lane))) {
578
+ } else if (
579
+ !includesSomeLane(renderLanes, (OffscreenLane: Lane)) ||
580
+ // Server renderer does not render hidden subtrees, so if we're hydrating
581
+ // we should always bail out and schedule a subsequent render pass, to
582
+ // force a client render. Even if we're already at Offscreen priority.
583
+ (current === null && getIsHydrating())
584
+ ) {
585
let nextBaseLanes;
586
if (prevState !== null) {
587
const prevBaseLanes = prevState.baseLanes;