diff --git a/packages/rax/src/constant.js b/packages/rax/src/constant.js index f17d5e0d6..5173f0c38 100644 --- a/packages/rax/src/constant.js +++ b/packages/rax/src/constant.js @@ -4,3 +4,5 @@ export const INTERNAL = '_internal'; export const INSTANCE = '_instance'; export const NATIVE_NODE = '_nativeNode'; export const RENDERED_COMPONENT = '_renderedComponent'; +export const SUSPENSE = Symbol.for('suspense'); +export const LAZY_TYPE = Symbol.for('lazy'); diff --git a/packages/rax/src/index.js b/packages/rax/src/index.js index e6ba641f0..ba3917b2c 100644 --- a/packages/rax/src/index.js +++ b/packages/rax/src/index.js @@ -8,13 +8,16 @@ export Fragment from './fragment'; export render from './render'; export Component, { PureComponent } from './vdom/component'; export version from './version'; +export lazy from './lazy'; import Host from './vdom/host'; import Instance from './vdom/instance'; import Element from './vdom/element'; import flattenChildren from './vdom/flattenChildren'; import DevtoolsHook from './devtools/index'; +import { SUSPENSE } from './constant'; +export const Suspense = SUSPENSE; export const shared = { Host, Instance, diff --git a/packages/rax/src/lazy.js b/packages/rax/src/lazy.js new file mode 100644 index 000000000..beecd53eb --- /dev/null +++ b/packages/rax/src/lazy.js @@ -0,0 +1,53 @@ +import { LAZY_TYPE } from './constant'; + +const Uninitialized = -1; +const Pending = 0; +const Resolved = 1; +const Rejected = 2; + +function lazyInitializer(payload) { + if (payload._status === Uninitialized) { + const ctor = payload._result; + const thenable = ctor(); // Transition to the next state. + + const pending = payload; + pending._status = Pending; + pending._result = thenable; + thenable.then((moduleObject) => { + if (payload._status === Pending) { + const defaultExport = moduleObject.default; + const resolved = payload; + resolved._status = Resolved; + resolved._result = defaultExport; + } + }, (error) => { + if (payload._status === Pending) { + // Transition to the next state. + const rejected = payload; + rejected._status = Rejected; + rejected._result = error; + } + }); + } + + if (payload._status === Resolved) { + return payload._result; + } else { + throw payload._result; + } +} + +export default function lazy(ctor) { + var payload = { + _status: -1, + _result: ctor + }; + + var lazyType = { + $$typeof: LAZY_TYPE, + _payload: payload, + _init: lazyInitializer + }; + + return lazyType; +} \ No newline at end of file diff --git a/packages/rax/src/vdom/getNewNativeNodeMounter.js b/packages/rax/src/vdom/getNewNativeNodeMounter.js new file mode 100644 index 000000000..96fd867cb --- /dev/null +++ b/packages/rax/src/vdom/getNewNativeNodeMounter.js @@ -0,0 +1,31 @@ +import Host from './host'; +import toArray from '../toArray'; + +export default function(prevNativeNode) { + let lastNativeNode = null; + + return (newNativeNode, parent) => { + const driver = Host.driver; + + prevNativeNode = toArray(prevNativeNode); + newNativeNode = toArray(newNativeNode); + + // If the new length large then prev + for (let i = 0; i < newNativeNode.length; i++) { + let nativeNode = newNativeNode[i]; + if (prevNativeNode[i]) { + driver.replaceChild(nativeNode, prevNativeNode[i]); + } else if (lastNativeNode) { + driver.insertAfter(nativeNode, lastNativeNode); + } else { + driver.appendChild(nativeNode, parent); + } + lastNativeNode = nativeNode; + } + + // If the new length less then prev + for (let i = newNativeNode.length; i < prevNativeNode.length; i++) { + driver.removeChild(prevNativeNode[i]); + } + }; +}; \ No newline at end of file diff --git a/packages/rax/src/vdom/inject.js b/packages/rax/src/vdom/inject.js index b23d54df2..5d72f3030 100644 --- a/packages/rax/src/vdom/inject.js +++ b/packages/rax/src/vdom/inject.js @@ -6,6 +6,7 @@ import CompositeComponent from './composite'; import FragmentComponent from './fragment'; import reconciler from '../devtools/reconciler'; import { throwError, throwMinifiedError } from '../error'; +import SuspenseComponent from './suspense'; export default function inject({ driver, measurer }) { // Inject component class @@ -14,6 +15,7 @@ export default function inject({ driver, measurer }) { Host.__Text = TextComponent; Host.__Fragment = FragmentComponent; Host.__Composite = CompositeComponent; + Host.__Suspense = SuspenseComponent; // Inject render driver if (!(Host.driver = driver || Host.driver)) { diff --git a/packages/rax/src/vdom/instantiateComponent.js b/packages/rax/src/vdom/instantiateComponent.js index 4f5d0e1ca..523d2dc4d 100644 --- a/packages/rax/src/vdom/instantiateComponent.js +++ b/packages/rax/src/vdom/instantiateComponent.js @@ -1,13 +1,30 @@ import Host from './host'; import {isString, isNumber, isArray, isNull, isPlainObject} from '../types'; import { throwMinifiedWarn, throwError } from '../error'; +import { SUSPENSE, LAZY_TYPE } from '../constant'; export default function instantiateComponent(element) { let instance; + // Lazy element (eg. server component) + if (element && element.$$typeof === LAZY_TYPE) { + const payload = element._payload; + const init = element._init; + element = init(payload); + } + if (isPlainObject(element) && element !== null && element.type) { - // Special case string values - if (isString(element.type)) { + // Lazy component + if (element.type.$$typeof === LAZY_TYPE) { + const payload = element.type._payload; + const init = element.type._init; + const type = init(payload); + + element.type = type; + instance = instantiateComponent(element); + } else if (element.type === SUSPENSE) { // Special case string values + instance = new Host.__Suspense(element); + } else if (isString(element.type)) { instance = new Host.__Native(element); } else { instance = new Host.__Composite(element); diff --git a/packages/rax/src/vdom/performInSandbox.js b/packages/rax/src/vdom/performInSandbox.js index fda3b71eb..5e78b9e82 100644 --- a/packages/rax/src/vdom/performInSandbox.js +++ b/packages/rax/src/vdom/performInSandbox.js @@ -23,6 +23,22 @@ export default function performInSandbox(fn, instance, callback) { * @param {*} error */ export function handleError(instance, error) { + const value = error; + if ( + value !== null && + typeof value === 'object' && + typeof value.then === 'function' + ) { + const suspenseBoundary = getNearestParent(instance, parent => { + return parent.type === Symbol.for('suspense'); + }); + + if (suspenseBoundary) { + suspenseBoundary.__handleError(suspenseBoundary, error); + return; + } + } + let boundary = getNearestParent(instance, parent => { return parent.componentDidCatch || parent.constructor && parent.constructor.getDerivedStateFromError; }); diff --git a/packages/rax/src/vdom/suspense.js b/packages/rax/src/vdom/suspense.js new file mode 100644 index 000000000..6d9d659d5 --- /dev/null +++ b/packages/rax/src/vdom/suspense.js @@ -0,0 +1,161 @@ +import BaseComponent from './base'; +import instantiateComponent from './instantiateComponent'; +import { INSTANCE, INTERNAL, RENDERED_COMPONENT, LAZY_TYPE } from '../constant'; +import updater from './updater'; +import performInSandbox from './performInSandbox'; +import getNewNativeNodeMounter from './getNewNativeNodeMounter'; +import shouldUpdateComponent from './shouldUpdateComponent'; + +/** + * Suspense Component + */ +class SuspenseComponent extends BaseComponent { + __mountComponent(parent, parentInstance, context, nativeNodeMounter) { + this.__initComponent(parent, parentInstance, context); + + const currentElement = this.__currentElement; + const publicProps = currentElement.props; + + let instance = this[INSTANCE] = {}; + instance[INTERNAL] = this; + + instance.props = publicProps; + instance.context = context; + instance.updater = updater; + instance.type = currentElement.type; + instance.nativeNodeMounter = nativeNodeMounter; + + instance.__handleError = this.__handleError; + + performInSandbox(() => { + const renderedElement = publicProps.children; + const renderedComponent = instantiateComponent(renderedElement); + renderedComponent.__mountComponent( + parent, + instance, + context, + nativeNodeMounter + ); + + if (this.__didCapture) { + // Unmount the broken component. + // Component render error will be caught by sandbox. + renderedComponent.unmountComponent(true); + } else { + this[RENDERED_COMPONENT] = renderedComponent; + } + }, instance, (error) => { + this.__handleError(instance, error); + }); + + return instance; + } + + __handleError(instance, error) { + if (!error.then) { + throw error; + } + + const { fallback, children } = instance.props; + const internal = instance[INTERNAL]; + + const wakeable = error; + wakeable.then(() => { + performInSandbox(() => { + if (!instance.__didMount) { + const prevRenderedComponent = internal[RENDERED_COMPONENT]; + internal.__mountRenderedComponent(children); + prevRenderedComponent.unmountComponent(true); + + instance.__didMount = true; + } else { + instance.updater.forceUpdate(instance); + } + }, instance, (e) => { + instance.__handleError(instance, e); + }); + }); + + if (!internal.__didCapture && !instance.__didMount) { + internal.__mountRenderedComponent(fallback); + internal.__didCapture = true; + } + } + + __mountRenderedComponent(component) { + const prevRenderedComponent = this[RENDERED_COMPONENT]; + + let nativeNodeMounter = this[INSTANCE].nativeNodeMounter; + + if (prevRenderedComponent && prevRenderedComponent.__currentElement) { + const prevNativeNode = prevRenderedComponent.__getNativeNode(); + nativeNodeMounter = getNewNativeNodeMounter(prevNativeNode); + } + + this[RENDERED_COMPONENT] = instantiateComponent(component); + this[RENDERED_COMPONENT].__mountComponent( + this._parent, + this[INSTANCE], + this._context, + nativeNodeMounter + ); + } + + __getNativeNode() { + let renderedComponent = this[RENDERED_COMPONENT]; + if (renderedComponent) { + return renderedComponent.__getNativeNode(); + } + } + + __updateComponent( + prevElement, + nextElement, + prevUnmaskedContext, + nextUnmaskedContext, + ) { + let instance = this[INSTANCE]; + + // Maybe update component that has already been unmounted or failed mount. + if (!instance) { + return; + } + + performInSandbox(() => { + let prevRenderedComponent = this[RENDERED_COMPONENT]; + let prevRenderedElement = prevRenderedComponent.__currentElement; + + // Replace with next + this.__currentElement = nextElement; + this._context = nextUnmaskedContext; + + instance.props = nextElement.props; + instance.context = nextUnmaskedContext; + + const { children } = nextElement.props || {}; + let nextRenderedElement = children; + + if (children && children.$$typeof === LAZY_TYPE) { + const payload = children._payload; + const init = children._init; + nextRenderedElement = init(payload); + } + + if (shouldUpdateComponent(prevRenderedElement, nextRenderedElement)) { + prevRenderedComponent.__updateComponent( + prevRenderedElement, + nextRenderedElement, + prevUnmaskedContext, + nextUnmaskedContext + ); + } else { + prevRenderedComponent.unmountComponent(true); + this.__mountRenderedComponent(nextRenderedElement); + } + }, instance, (error) => { + this.__handleError(instance, error); + }); + } +} + +export default SuspenseComponent;