Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for componentDidCatch + getDerivedStateFromError #305

Merged
merged 1 commit into from
Jul 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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