diff --git a/.changeset/pink-gifts-kneel.md b/.changeset/pink-gifts-kneel.md index b73ff682..7bc5c1f4 100644 --- a/.changeset/pink-gifts-kneel.md +++ b/.changeset/pink-gifts-kneel.md @@ -1,6 +1,7 @@ --- -"preact-render-to-string": major +"preact-render-to-string": minor --- -Allow prepass like behavior in renderToString where a Promise -will be awaited and then continued +Allow prepass like behavior where a Promise +will be awaited and then continued, this is done with +the new `renderToStringAsync` export diff --git a/src/index.d.ts b/src/index.d.ts index 32a4ca7a..81db2bd7 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -3,17 +3,15 @@ import { VNode } from 'preact'; export default function renderToString

( vnode: VNode

, context?: any -): string | Promise; +): string; -export function render

( - vnode: VNode

, - context?: any -): string | Promise; -export function renderToString

( +export function render

(vnode: VNode

, context?: any): string; +export function renderToString

(vnode: VNode

, context?: any): string; +export function renderToStringAsync

( vnode: VNode

, context?: any ): string | Promise; export function renderToStaticMarkup

( vnode: VNode

, context?: any -): string | Promise; +): string; diff --git a/src/index.js b/src/index.js index 771f04ce..19567c65 100644 --- a/src/index.js +++ b/src/index.js @@ -54,13 +54,62 @@ export function renderToString(vnode, context) { const parent = h(Fragment, null); parent[CHILDREN] = [vnode]; + try { + return _renderToString( + vnode, + context || EMPTY_OBJ, + false, + undefined, + parent, + false + ); + } catch (e) { + if (e.then) { + throw new Error('Use "renderToStringAsync" for suspenseful rendering.'); + } + + throw e; + } finally { + // options._commit, we don't schedule any effects in this library right now, + // so we can pass an empty queue to this hook. + if (options[COMMIT]) options[COMMIT](vnode, EMPTY_ARR); + options[SKIP_EFFECTS] = previousSkipEffects; + EMPTY_ARR.length = 0; + } +} + +/** + * Render Preact JSX + Components to an HTML string. + * @param {VNode} vnode JSX Element / VNode to render + * @param {Object} [context={}] Initial root context object + * @returns {string} serialized HTML + */ +export function renderToStringAsync(vnode, context) { + // Performance optimization: `renderToString` is synchronous and we + // therefore don't execute any effects. To do that we pass an empty + // array to `options._commit` (`__c`). But we can go one step further + // and avoid a lot of dirty checks and allocations by setting + // `options._skipEffects` (`__s`) too. + const previousSkipEffects = options[SKIP_EFFECTS]; + options[SKIP_EFFECTS] = true; + + // store options hooks once before each synchronous render call + beforeDiff = options[DIFF]; + afterDiff = options[DIFFED]; + renderHook = options[RENDER]; + ummountHook = options.unmount; + + const parent = h(Fragment, null); + parent[CHILDREN] = [vnode]; + try { const rendered = _renderToString( vnode, context || EMPTY_OBJ, false, undefined, - parent + parent, + true ); if (Array.isArray(rendered)) { @@ -143,9 +192,17 @@ function renderClassComponent(vnode, context) { * @param {boolean} isSvgMode * @param {any} selectValue * @param {VNode} parent + * @param {boolean} asyncMode * @returns {string | Promise | (string | Promise)[]} */ -function _renderToString(vnode, context, isSvgMode, selectValue, parent) { +function _renderToString( + vnode, + context, + isSvgMode, + selectValue, + parent, + asyncMode +) { // Ignore non-rendered VNodes/values if (vnode == null || vnode === true || vnode === false || vnode === '') { return ''; @@ -171,7 +228,8 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) { context, isSvgMode, selectValue, - parent + parent, + asyncMode ); if (typeof childRender === 'string') { @@ -235,7 +293,8 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) { context, isSvgMode, selectValue, - vnode + vnode, + asyncMode ); } else { // Values are pre-escaped by the JSX transform @@ -315,7 +374,8 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) { context, isSvgMode, selectValue, - vnode + vnode, + asyncMode ); return str; } catch (err) { @@ -346,7 +406,8 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) { context, isSvgMode, selectValue, - vnode + vnode, + asyncMode ); } @@ -373,7 +434,8 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) { context, isSvgMode, selectValue, - vnode + vnode, + asyncMode ); if (afterDiff) afterDiff(vnode); vnode[PARENT] = undefined; @@ -382,10 +444,19 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) { return str; } catch (error) { + if (!asyncMode) throw error; + if (!error || typeof error.then !== 'function') throw error; return error.then(() => - _renderToString(rendered, context, isSvgMode, selectValue, vnode) + _renderToString( + rendered, + context, + isSvgMode, + selectValue, + vnode, + asyncMode + ) ); } } @@ -517,7 +588,14 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) { // recurse into this element VNode's children let childSvgMode = type === 'svg' || (type !== 'foreignObject' && isSvgMode); - html = _renderToString(children, context, childSvgMode, selectValue, vnode); + html = _renderToString( + children, + context, + childSvgMode, + selectValue, + vnode, + asyncMode + ); } if (afterDiff) afterDiff(vnode);