Skip to content

Commit

Permalink
Hide timed-out Suspense children
Browse files Browse the repository at this point in the history
When a subtree takes too long to load, we swap its contents out for
a fallback to unblock the rest of the tree. Because we don't want
to lose the state of the timed out view, we shouldn't actually delete
the nodes from the tree. Instead, we'll keep them mounted and hide
them visually. When the subtree is unblocked, we un-hide it, having
preserved the existing state.

Adds additional host config methods. For mutation mode:

- hideInstance
- hideTextInstance
- unhideInstance
- unhideTextInstance

For persistent mode:

- cloneHiddenInstance
- cloneUnhiddenInstance
- createHiddenTextInstance

I've only implemented the new methods in the noop and test renderers.
I'll implement them in the other renderers in subsequent commits.
  • Loading branch information
acdlite committed Oct 12, 2018
1 parent 3a85496 commit d9b589b
Show file tree
Hide file tree
Showing 18 changed files with 767 additions and 85 deletions.
2 changes: 1 addition & 1 deletion packages/jest-react/src/JestReact.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ function jsonChildrenToJSXChildren(jsonChildren) {
let allJSXChildrenAreStrings = true;
let jsxChildrenString = '';
for (let i = 0; i < jsonChildren.length; i++) {
const jsxChild = jsonChildrenToJSXChildren(jsonChildren[i]);
const jsxChild = jsonChildToJSXChild(jsonChildren[i]);
jsxChildren.push(jsxChild);
if (allJSXChildrenAreStrings) {
if (typeof jsxChild === 'string') {
Expand Down
16 changes: 16 additions & 0 deletions packages/react-art/src/ReactARTHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -405,3 +405,19 @@ export function commitUpdate(
) {
instance._applyProps(instance, newProps, oldProps);
}

export function hideInstance(instance: Instance): void {
throw new Error('Not yet implemented.');
}

export function hideTextInstance(textInstance: TextInstance): void {
throw new Error('Not yet implemented.');
}

export function unhideInstance(instance: Instance, props: Props): void {
throw new Error('Not yet implemented.');
}

export function unhideTextInstance(textInstance: TextInstance): void {
throw new Error('Not yet implemented.');
}
16 changes: 16 additions & 0 deletions packages/react-dom/src/client/ReactDOMHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,22 @@ export function removeChildFromContainer(
}
}

export function hideInstance(instance: Instance): void {
throw new Error('TODO');
}

export function hideTextInstance(textInstance: TextInstance): void {
throw new Error('TODO');
}

export function unhideInstance(instance: Instance, props: Props): void {
throw new Error('TODO');
}

export function unhideTextInstance(textInstance: TextInstance): void {
throw new Error('TODO');
}

// -------------------
// Hydration
// -------------------
Expand Down
27 changes: 27 additions & 0 deletions packages/react-native-renderer/src/ReactFabricHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,33 @@ export function cloneInstance(
};
}

export function cloneHiddenInstance(
instance: Instance,
type: string,
props: Props,
internalInstanceHandle: Object,
): Instance {
throw new Error('Not yet implemented.');
}

export function cloneUnhiddenInstance(
instance: Instance,
type: string,
props: Props,
internalInstanceHandle: Object,
): Instance {
throw new Error('Not yet implemented.');
}

export function createHiddenTextInstance(
text: string,
rootContainerInstance: Container,
hostContext: HostContext,
internalInstanceHandle: Object,
): TextInstance {
throw new Error('Not yet implemented.');
}

export function createContainerChildSet(container: Container): ChildSet {
return createChildNodeSet(container);
}
Expand Down
16 changes: 16 additions & 0 deletions packages/react-native-renderer/src/ReactNativeHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -451,3 +451,19 @@ export function removeChildFromContainer(
export function resetTextContent(instance: Instance): void {
// Noop
}

export function hideInstance(instance: Instance): void {
throw new Error('Not yet implemented.');
}

export function hideTextInstance(textInstance: TextInstance): void {
throw new Error('Not yet implemented.');
}

export function unhideInstance(instance: Instance, props: Props): void {
throw new Error('Not yet implemented.');
}

export function unhideTextInstance(textInstance: TextInstance): void {
throw new Error('Not yet implemented.');
}
73 changes: 73 additions & 0 deletions packages/react-noop-renderer/src/createReactNoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,24 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
removeChild,
removeChildFromContainer,

hideInstance(instance: Instance): void {
instance.hidden = true;
},

hideTextInstance(textInstance: TextInstance): void {
textInstance.hidden = true;
},

unhideInstance(instance: Instance, props: Props): void {
if (!props.hidden) {
instance.hidden = false;
}
},

unhideTextInstance(textInstance: TextInstance): void {
textInstance.hidden = false;
},

resetTextContent(instance: Instance): void {
instance.text = null;
},
Expand Down Expand Up @@ -429,6 +447,61 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
): void {
container.children = newChildren;
},

cloneHiddenInstance(
instance: Instance,
type: string,
props: Props,
internalInstanceHandle: Object,
): Instance {
const clone = cloneInstance(
instance,
null,
type,
props,
props,
internalInstanceHandle,
true,
null,
);
clone.hidden = true;
return clone;
},

cloneUnhiddenInstance(
instance: Instance,
type: string,
props: Props,
internalInstanceHandle: Object,
): Instance {
const clone = cloneInstance(
instance,
null,
type,
props,
props,
internalInstanceHandle,
true,
null,
);
clone.hidden = props.hidden;
return clone;
},

createHiddenTextInstance(
text: string,
rootContainerInstance: Container,
hostContext: Object,
internalInstanceHandle: Object,
): TextInstance {
const inst = {text: text, id: instanceCounter++, hidden: true};
// Hide from unit tests
Object.defineProperty(inst, 'id', {
value: inst.id,
enumerable: false,
});
return inst;
},
};

const NoopRenderer = reconciler(hostConfig);
Expand Down
3 changes: 0 additions & 3 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -977,9 +977,6 @@ function updateSuspenseComponent(
nextState.alreadyCaptured = true;
nextState.didTimeout = true;
}
// If this render commits, schedule an update effect to record the timed-
// out time.
workInProgress.effectTag |= Update;
}
}

Expand Down
82 changes: 73 additions & 9 deletions packages/react-reconciler/src/ReactFiberCommitWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
Placement,
Snapshot,
Update,
Callback,
} from 'shared/ReactSideEffectTags';
import getComponentName from 'shared/getComponentName';
import invariant from 'shared/invariant';
Expand Down Expand Up @@ -72,6 +73,10 @@ import {
removeChildFromContainer,
replaceContainerChildren,
createContainerChildSet,
hideInstance,
hideTextInstance,
unhideInstance,
unhideTextInstance,
} from './ReactFiberHostConfig';
import {
captureCommitPhaseError,
Expand Down Expand Up @@ -349,27 +354,46 @@ function commitLifeCycles(
return;
}
case SuspenseComponent: {
let newState: SuspenseState | null = finishedWork.memoizedState;
if (newState === null) {
if (finishedWork.effectTag & Callback) {
// In non-strict mode, a suspense boundary times out by commiting
// twice: first, by committing the children in an inconsistent state,
// then hiding them and showing the fallback children in a subsequent
// commit.
newState = finishedWork.memoizedState = {
const newState: SuspenseState = {
alreadyCaptured: true,
didTimeout: false,
timedOutAt: NoWork,
};
finishedWork.memoizedState = newState;
scheduleWork(finishedWork, Sync);
return;
}
let oldState: SuspenseState | null =
current !== null ? current.memoizedState : null;
let newState: SuspenseState | null = finishedWork.memoizedState;
let oldDidTimeout = oldState !== null ? oldState.didTimeout : false;

let newDidTimeout;
let primaryChildParent = finishedWork;
if (newState === null) {
newDidTimeout = false;
} else {
newState.alreadyCaptured = false;
if (newState.timedOutAt === NoWork) {
// If the children had not already timed out, record the time.
// This is used to compute the elapsed time during subsequent
// attempts to render the children.
newState.timedOutAt = requestCurrentTime();
newDidTimeout = newState.didTimeout;
if (newDidTimeout) {
primaryChildParent = finishedWork.child;
newState.alreadyCaptured = false;
if (newState.timedOutAt === NoWork) {
// If the children had not already timed out, record the time.
// This is used to compute the elapsed time during subsequent
// attempts to render the children.
newState.timedOutAt = requestCurrentTime();
}
}
}

if (newDidTimeout !== oldDidTimeout && primaryChildParent !== null) {
hideOrUnhideAllChildren(primaryChildParent, newDidTimeout);
}
return;
}
default: {
Expand All @@ -382,6 +406,46 @@ function commitLifeCycles(
}
}

function hideOrUnhideAllChildren(finishedWork, isHidden) {
if (supportsMutation) {
// We only have the top Fiber that was inserted but we need recurse down its
// children to find all the terminal nodes.
let node: Fiber = finishedWork;
while (true) {
if (node.tag === HostComponent) {
const instance = node.stateNode;
if (isHidden) {
hideInstance(instance);
} else {
unhideInstance(node.stateNode, node.memoizedProps);
}
} else if (node.tag === HostText) {
const instance = node.stateNode;
if (isHidden) {
hideTextInstance(instance);
} else {
unhideTextInstance(instance);
}
} else if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
if (node === finishedWork) {
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === finishedWork) {
return;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
}
}

function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref;
if (ref !== null) {
Expand Down
Loading

0 comments on commit d9b589b

Please sign in to comment.