Skip to content

Commit bbbb9ca

Browse files
Merge pull request #305 from preactjs/error-boundary-2
2 parents d251f40 + 568f139 commit bbbb9ca

File tree

4 files changed

+375
-32
lines changed

4 files changed

+375
-32
lines changed

.changeset/cold-otters-argue.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'preact-render-to-string': minor
3+
---
4+
5+
Add support for error boundaries via `componentDidCatch` and `getDerivedStateFromError`
6+
7+
This feature is disabled by default and can be enabled by toggling the `errorBoundaries` option:
8+
9+
```js
10+
import { options } from 'preact';
11+
12+
// Enable error boundaries
13+
options.errorBoundaries = true;
14+
```

README.md

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,17 @@ app.get('/:fox', (req, res) => {
8484
});
8585
```
8686

87+
### Error Boundaries
88+
89+
Rendering errors can be caught by Preact via `getDerivedStateFromErrors` or `componentDidCatch`. To enable that feature in `preact-render-to-string` set `errorBoundaries = true`
90+
91+
```js
92+
import { options } from 'preact';
93+
94+
// Enable error boundaries in `preact-render-to-string`
95+
options.errorBoundaries = true;
96+
```
97+
8798
---
8899

89100
### `Suspense` & `lazy` components with [`preact/compat`](https://www.npmjs.com/package/preact) & [`preact-ssr-prepass`](https://www.npmjs.com/package/preact-ssr-prepass)
@@ -94,50 +105,48 @@ npm install preact preact-render-to-string preact-ssr-prepass
94105

95106
```jsx
96107
export default () => {
97-
return (
98-
<h1>Home page</h1>
99-
)
100-
}
108+
return <h1>Home page</h1>;
109+
};
101110
```
102111

103112
```jsx
104-
import { Suspense, lazy } from "preact/compat"
113+
import { Suspense, lazy } from 'preact/compat';
105114

106115
// Creation of the lazy component
107-
const HomePage = lazy(() => import("./pages/home"))
116+
const HomePage = lazy(() => import('./pages/home'));
108117

109118
const Main = () => {
110-
return (
111-
<Suspense fallback={<p>Loading</p>}>
112-
<HomePage />
113-
</Suspense>
114-
)
115-
}
119+
return (
120+
<Suspense fallback={<p>Loading</p>}>
121+
<HomePage />
122+
</Suspense>
123+
);
124+
};
116125
```
117126

118127
```jsx
119-
import { render } from "preact-render-to-string"
120-
import prepass from "preact-ssr-prepass"
121-
import { Main } from "./main"
128+
import { render } from 'preact-render-to-string';
129+
import prepass from 'preact-ssr-prepass';
130+
import { Main } from './main';
122131

123132
const main = async () => {
124-
// Creation of the virtual DOM
125-
const vdom = <Main />
126-
127-
// Pre-rendering of lazy components
128-
await prepass(vdom)
129-
130-
// Rendering of components
131-
const html = render(vdom)
132-
133-
console.log(html)
134-
// <h1>Home page</h1>
135-
}
133+
// Creation of the virtual DOM
134+
const vdom = <Main />;
135+
136+
// Pre-rendering of lazy components
137+
await prepass(vdom);
138+
139+
// Rendering of components
140+
const html = render(vdom);
141+
142+
console.log(html);
143+
// <h1>Home page</h1>
144+
};
136145

137146
// Execution & error handling
138-
main().catch(error => {
139-
console.error(error)
140-
})
147+
main().catch((error) => {
148+
console.error(error);
149+
});
141150
```
142151

143152
---

src/index.js

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,15 @@ const EMPTY_OBJ = {};
7878
function renderClassComponent(vnode, context) {
7979
let type = /** @type {import("preact").ComponentClass<typeof vnode.props>} */ (vnode.type);
8080

81-
let c = new type(vnode.props, context);
81+
let isMounting = true;
82+
let c;
83+
if (vnode[COMPONENT]) {
84+
isMounting = false;
85+
c = vnode[COMPONENT];
86+
c.state = c[NEXT_STATE];
87+
} else {
88+
c = new type(vnode.props, context);
89+
}
8290

8391
vnode[COMPONENT] = c;
8492
c[VNODE] = vnode;
@@ -100,12 +108,14 @@ function renderClassComponent(vnode, context) {
100108
c.state,
101109
type.getDerivedStateFromProps(c.props, c.state)
102110
);
103-
} else if (c.componentWillMount) {
111+
} else if (isMounting && c.componentWillMount) {
104112
c.componentWillMount();
105113

106114
// If the user called setState in cWM we need to flush pending,
107115
// state updates. This is the same behaviour in React.
108116
c.state = c[NEXT_STATE] !== c.state ? c[NEXT_STATE] : c.state;
117+
} else if (!isMounting && c.componentWillUpdate) {
118+
c.componentWillUpdate();
109119
}
110120

111121
if (renderHook) renderHook(vnode);
@@ -215,6 +225,69 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
215225
if (component.getChildContext != null) {
216226
context = assign({}, context, component.getChildContext());
217227
}
228+
229+
if (
230+
(type.getDerivedStateFromError || component.componentDidCatch) &&
231+
options.errorBoundaries
232+
) {
233+
let str = '';
234+
// When a component returns a Fragment node we flatten it in core, so we
235+
// need to mirror that logic here too
236+
let isTopLevelFragment =
237+
rendered != null &&
238+
rendered.type === Fragment &&
239+
rendered.key == null;
240+
rendered = isTopLevelFragment ? rendered.props.children : rendered;
241+
242+
try {
243+
str = _renderToString(
244+
rendered,
245+
context,
246+
isSvgMode,
247+
selectValue,
248+
vnode
249+
);
250+
return str;
251+
} catch (err) {
252+
if (type.getDerivedStateFromError) {
253+
component[NEXT_STATE] = type.getDerivedStateFromError(err);
254+
}
255+
256+
if (component.componentDidCatch) {
257+
component.componentDidCatch(err, {});
258+
}
259+
260+
if (component[DIRTY]) {
261+
rendered = renderClassComponent(vnode, context);
262+
component = vnode[COMPONENT];
263+
264+
if (component.getChildContext != null) {
265+
context = assign({}, context, component.getChildContext());
266+
}
267+
268+
let isTopLevelFragment =
269+
rendered != null &&
270+
rendered.type === Fragment &&
271+
rendered.key == null;
272+
rendered = isTopLevelFragment ? rendered.props.children : rendered;
273+
274+
str = _renderToString(
275+
rendered,
276+
context,
277+
isSvgMode,
278+
selectValue,
279+
vnode
280+
);
281+
}
282+
283+
return str;
284+
} finally {
285+
if (afterDiff) afterDiff(vnode);
286+
vnode[PARENT] = undefined;
287+
288+
if (ummountHook) ummountHook(vnode);
289+
}
290+
}
218291
}
219292

220293
// When a component returns a Fragment node we flatten it in core, so we

0 commit comments

Comments
 (0)