Skip to content

Commit

Permalink
Add support for componentDidCatch + getDerivedStateFromError
Browse files Browse the repository at this point in the history
  • Loading branch information
marvinhagemeister committed Jul 10, 2023
1 parent cbe881a commit 568f139
Show file tree
Hide file tree
Showing 4 changed files with 375 additions and 32 deletions.
14 changes: 14 additions & 0 deletions .changeset/cold-otters-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'preact-render-to-string': minor
---

Add support for error boundaries via `componentDidCatch` and `getDerivedStateFromError`

This feature is disabled by default and can be enabled by toggling the `errorBoundaries` option:

```js
import { options } from 'preact';

// Enable error boundaries
options.errorBoundaries = true;
```
69 changes: 39 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,17 @@ app.get('/:fox', (req, res) => {
});
```

### Error Boundaries

Rendering errors can be caught by Preact via `getDerivedStateFromErrors` or `componentDidCatch`. To enable that feature in `preact-render-to-string` set `errorBoundaries = true`

```js
import { options } from 'preact';

// Enable error boundaries in `preact-render-to-string`
options.errorBoundaries = true;
```

---

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

```jsx
export default () => {
return (
<h1>Home page</h1>
)
}
return <h1>Home page</h1>;
};
```

```jsx
import { Suspense, lazy } from "preact/compat"
import { Suspense, lazy } from 'preact/compat';

// Creation of the lazy component
const HomePage = lazy(() => import("./pages/home"))
const HomePage = lazy(() => import('./pages/home'));

const Main = () => {
return (
<Suspense fallback={<p>Loading</p>}>
<HomePage />
</Suspense>
)
}
return (
<Suspense fallback={<p>Loading</p>}>
<HomePage />
</Suspense>
);
};
```

```jsx
import { render } from "preact-render-to-string"
import prepass from "preact-ssr-prepass"
import { Main } from "./main"
import { render } from 'preact-render-to-string';
import prepass from 'preact-ssr-prepass';
import { Main } from './main';

const main = async () => {
// Creation of the virtual DOM
const vdom = <Main />
// Pre-rendering of lazy components
await prepass(vdom)
// Rendering of components
const html = render(vdom)
console.log(html)
// <h1>Home page</h1>
}
// Creation of the virtual DOM
const vdom = <Main />;

// Pre-rendering of lazy components
await prepass(vdom);

// Rendering of components
const html = render(vdom);

console.log(html);
// <h1>Home page</h1>
};

// Execution & error handling
main().catch(error => {
console.error(error)
})
main().catch((error) => {
console.error(error);
});
```

---
Expand Down
77 changes: 75 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,15 @@ const EMPTY_OBJ = {};
function renderClassComponent(vnode, context) {
let type = /** @type {import("preact").ComponentClass<typeof vnode.props>} */ (vnode.type);

let c = new type(vnode.props, context);
let isMounting = true;
let c;
if (vnode[COMPONENT]) {
isMounting = false;
c = vnode[COMPONENT];
c.state = c[NEXT_STATE];
} else {
c = new type(vnode.props, context);
}

vnode[COMPONENT] = c;
c[VNODE] = vnode;
Expand All @@ -100,12 +108,14 @@ function renderClassComponent(vnode, context) {
c.state,
type.getDerivedStateFromProps(c.props, c.state)
);
} else if (c.componentWillMount) {
} else if (isMounting && c.componentWillMount) {
c.componentWillMount();

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

if (renderHook) renderHook(vnode);
Expand Down Expand Up @@ -215,6 +225,69 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
if (component.getChildContext != null) {
context = assign({}, context, component.getChildContext());
}

if (
(type.getDerivedStateFromError || component.componentDidCatch) &&
options.errorBoundaries
) {
let str = '';
// When a component returns a Fragment node we flatten it in core, so we
// need to mirror that logic here too
let isTopLevelFragment =
rendered != null &&
rendered.type === Fragment &&
rendered.key == null;
rendered = isTopLevelFragment ? rendered.props.children : rendered;

try {
str = _renderToString(
rendered,
context,
isSvgMode,
selectValue,
vnode
);
return str;
} catch (err) {
if (type.getDerivedStateFromError) {
component[NEXT_STATE] = type.getDerivedStateFromError(err);
}

if (component.componentDidCatch) {
component.componentDidCatch(err, {});
}

if (component[DIRTY]) {
rendered = renderClassComponent(vnode, context);
component = vnode[COMPONENT];

if (component.getChildContext != null) {
context = assign({}, context, component.getChildContext());
}

let isTopLevelFragment =
rendered != null &&
rendered.type === Fragment &&
rendered.key == null;
rendered = isTopLevelFragment ? rendered.props.children : rendered;

str = _renderToString(
rendered,
context,
isSvgMode,
selectValue,
vnode
);
}

return str;
} finally {
if (afterDiff) afterDiff(vnode);
vnode[PARENT] = undefined;

if (ummountHook) ummountHook(vnode);
}
}
}

// When a component returns a Fragment node we flatten it in core, so we
Expand Down
Loading

0 comments on commit 568f139

Please sign in to comment.