UNPKG

5.9 kBJavaScriptView Raw
1import { Component, createElement, options, Fragment } from 'preact';
2import { assign } from './util';
3
4const oldCatchError = options._catchError;
5options._catchError = function(error, newVNode, oldVNode) {
6 if (error.then) {
7 /** @type {import('./internal').Component} */
8 let component;
9 let vnode = newVNode;
10
11 for (; (vnode = vnode._parent); ) {
12 if ((component = vnode._component) && component._childDidSuspend) {
13 if (newVNode._dom == null) {
14 newVNode._dom = oldVNode._dom;
15 newVNode._children = oldVNode._children;
16 }
17 // Don't call oldCatchError if we found a Suspense
18 return component._childDidSuspend(error, newVNode._component);
19 }
20 }
21 }
22 oldCatchError(error, newVNode, oldVNode);
23};
24
25function detachedClone(vnode) {
26 if (vnode) {
27 if (vnode._component && vnode._component.__hooks) {
28 vnode._component.__hooks._list.forEach(effect => {
29 if (typeof effect._cleanup == 'function') effect._cleanup();
30 });
31
32 vnode._component.__hooks = null;
33 }
34
35 vnode = assign({}, vnode);
36 vnode._component = null;
37 vnode._children = vnode._children && vnode._children.map(detachedClone);
38 }
39
40 return vnode;
41}
42
43function removeOriginal(vnode) {
44 if (vnode) {
45 vnode._original = null;
46 vnode._children = vnode._children && vnode._children.map(removeOriginal);
47 }
48 return vnode;
49}
50
51// having custom inheritance instead of a class here saves a lot of bytes
52export function Suspense() {
53 // we do not call super here to golf some bytes...
54 this._pendingSuspensionCount = 0;
55 this._suspenders = null;
56 this._detachOnNextRender = null;
57}
58
59// Things we do here to save some bytes but are not proper JS inheritance:
60// - call `new Component()` as the prototype
61// - do not set `Suspense.prototype.constructor` to `Suspense`
62Suspense.prototype = new Component();
63
64/**
65 * @param {Promise} promise The thrown promise
66 * @param {Component<any, any>} suspendingComponent The suspending component
67 */
68Suspense.prototype._childDidSuspend = function(promise, suspendingComponent) {
69 /** @type {import('./internal').SuspenseComponent} */
70 const c = this;
71
72 if (c._suspenders == null) {
73 c._suspenders = [];
74 }
75 c._suspenders.push(suspendingComponent);
76
77 const resolve = suspended(c._vnode);
78
79 let resolved = false;
80 const onResolved = () => {
81 if (resolved) return;
82
83 resolved = true;
84 suspendingComponent.componentWillUnmount =
85 suspendingComponent._suspendedComponentWillUnmount;
86
87 if (resolve) {
88 resolve(onSuspensionComplete);
89 } else {
90 onSuspensionComplete();
91 }
92 };
93
94 suspendingComponent._suspendedComponentWillUnmount =
95 suspendingComponent.componentWillUnmount;
96 suspendingComponent.componentWillUnmount = () => {
97 onResolved();
98
99 if (suspendingComponent._suspendedComponentWillUnmount) {
100 suspendingComponent._suspendedComponentWillUnmount();
101 }
102 };
103
104 const onSuspensionComplete = () => {
105 if (!--c._pendingSuspensionCount) {
106 c._vnode._children[0] = removeOriginal(c.state._suspended);
107 c.setState({ _suspended: (c._detachOnNextRender = null) });
108
109 let suspended;
110 while ((suspended = c._suspenders.pop())) {
111 suspended.forceUpdate();
112 }
113 }
114 };
115
116 /**
117 * We do not set `suspended: true` during hydration because we want the actual markup
118 * to remain on screen and hydrate it when the suspense actually gets resolved.
119 * While in non-hydration cases the usual fallback -> component flow would occour.
120 */
121 const vnode = c._vnode;
122 const wasHydrating = vnode && vnode._hydrating === true;
123 if (!wasHydrating && !c._pendingSuspensionCount++) {
124 c.setState({ _suspended: (c._detachOnNextRender = c._vnode._children[0]) });
125 }
126 promise.then(onResolved, onResolved);
127};
128
129Suspense.prototype.componentWillUnmount = function() {
130 this._suspenders = [];
131};
132
133Suspense.prototype.render = function(props, state) {
134 if (this._detachOnNextRender) {
135 // When the Suspense's _vnode was created by a call to createVNode
136 // (i.e. due to a setState further up in the tree)
137 // it's _children prop is null, in this case we "forget" about the parked vnodes to detach
138 if (this._vnode._children)
139 this._vnode._children[0] = detachedClone(this._detachOnNextRender);
140 this._detachOnNextRender = null;
141 }
142
143 // Wrap fallback tree in a VNode that prevents itself from being marked as aborting mid-hydration:
144 const fallback =
145 state._suspended && createElement(Fragment, null, props.fallback);
146 if (fallback) fallback._hydrating = null;
147
148 return [
149 createElement(Fragment, null, state._suspended ? null : props.children),
150 fallback
151 ];
152};
153
154/**
155 * Checks and calls the parent component's _suspended method, passing in the
156 * suspended vnode. This is a way for a parent (e.g. SuspenseList) to get notified
157 * that one of its children/descendants suspended.
158 *
159 * The parent MAY return a callback. The callback will get called when the
160 * suspension resolves, notifying the parent of the fact.
161 * Moreover, the callback gets function `unsuspend` as a parameter. The resolved
162 * child descendant will not actually get unsuspended until `unsuspend` gets called.
163 * This is a way for the parent to delay unsuspending.
164 *
165 * If the parent does not return a callback then the resolved vnode
166 * gets unsuspended immediately when it resolves.
167 *
168 * @param {import('../src/internal').VNode} vnode
169 * @returns {((unsuspend: () => void) => void)?}
170 */
171export function suspended(vnode) {
172 let component = vnode._parent._component;
173 return component && component._suspended && component._suspended(vnode);
174}
175
176export function lazy(loader) {
177 let prom;
178 let component;
179 let error;
180
181 function Lazy(props) {
182 if (!prom) {
183 prom = loader();
184 prom.then(
185 exports => {
186 component = exports.default || exports;
187 },
188 e => {
189 error = e;
190 }
191 );
192 }
193
194 if (error) {
195 throw error;
196 }
197
198 if (!component) {
199 throw prom;
200 }
201
202 return createElement(component, props);
203 }
204
205 Lazy.displayName = 'Lazy';
206 Lazy._forwarded = true;
207 return Lazy;
208}