diff --git a/packages/rax-server-renderer/package.json b/packages/rax-server-renderer/package.json
index 6f2d39c32d..cd1e7e1abb 100644
--- a/packages/rax-server-renderer/package.json
+++ b/packages/rax-server-renderer/package.json
@@ -1,6 +1,6 @@
{
"name": "rax-server-renderer",
- "version": "1.2.1",
+ "version": "1.3.0",
"description": "Rax renderer for server-side render.",
"license": "BSD-3-Clause",
"main": "lib/index.js",
diff --git a/packages/rax-server-renderer/src/__tests__/renderToString.js b/packages/rax-server-renderer/src/__tests__/renderToString.js
index 76889ee84a..72082aff3f 100644
--- a/packages/rax-server-renderer/src/__tests__/renderToString.js
+++ b/packages/rax-server-renderer/src/__tests__/renderToString.js
@@ -1,6 +1,6 @@
/* @jsx createElement */
-import {createElement, useState, useEffect, createContext, useContext, useReducer} from 'rax';
+import {createElement, Component, useState, useEffect, createContext, useContext, useReducer} from 'rax';
import {renderToString} from '../index';
describe('renderToString', () => {
@@ -353,4 +353,72 @@ describe('renderToString', () => {
const str = renderToString();
expect(str).toBe('
light
');
});
+
+ it('should catch error with componentDidCatch', function() {
+ class ErrorBoundary extends Component {
+ constructor(props) {
+ super(props);
+ }
+
+ componentDidCatch(error, errorInfo) {
+ // log error
+ }
+
+ render() {
+ return this.props.children;
+ }
+ }
+
+ function MyWidget() {
+ throw new Error('widget error');
+ }
+
+ function App() {
+ return (
+
+
+
+
+
+ );
+ };
+
+ const str = renderToString();
+ expect(str).toBe('');
+ });
+
+ it('should call componentDidCatch when catch error', function() {
+ const mockFn = jest.fn();
+ class ErrorBoundary extends Component {
+ constructor(props) {
+ super(props);
+ }
+
+ componentDidCatch(error, errorInfo) {
+ mockFn();
+ }
+
+ render() {
+ return this.props.children;
+ }
+ }
+
+ function MyWidget() {
+ throw new Error('widget error');
+ }
+
+ function App() {
+ return (
+
+
+
+
+
+ );
+ };
+
+ const str = renderToString();
+ expect(mockFn).toHaveBeenCalled();
+ expect(str).toBe('');
+ });
});
diff --git a/packages/rax-server-renderer/src/index.js b/packages/rax-server-renderer/src/index.js
index 8e036696df..b2ad87341f 100644
--- a/packages/rax-server-renderer/src/index.js
+++ b/packages/rax-server-renderer/src/index.js
@@ -22,6 +22,7 @@ const VOID_ELEMENTS = {
};
const TEXT_SPLIT_COMMENT = '';
+const ERROR_COMMENT = '';
const ESCAPE_LOOKUP = {
'&': '&',
@@ -357,8 +358,11 @@ class ServerRenderer {
const type = element.type;
if (type) {
+ const isClassComponent = type.prototype && type.prototype.render;
+ const isFunctionComponent = typeof type === 'function';
+
// class component || function component
- if (type.prototype && type.prototype.render || typeof type === 'function') {
+ if (isClassComponent || isFunctionComponent) {
const instance = createInstance(element, context);
const currentComponent = {
@@ -399,7 +403,16 @@ class ServerRenderer {
// Reset owner after render, or it will casue memory leak.
shared.Host.owner = null;
- return this.renderElementToString(renderedElement, currentContext);
+ if (isClassComponent && instance.componentDidCatch) {
+ try {
+ return this.renderElementToString(renderedElement, currentContext);
+ } catch(e) {
+ instance.componentDidCatch(e);
+ return ERROR_COMMENT;
+ }
+ } else {
+ return this.renderElementToString(renderedElement, currentContext);
+ }
} else if (typeof type === 'string') {
// shoud set the identifier to false before render child
this.previousWasTextNode = false;
diff --git a/packages/rax/package.json b/packages/rax/package.json
index 9940d54e7e..7d41a505c6 100644
--- a/packages/rax/package.json
+++ b/packages/rax/package.json
@@ -1,6 +1,6 @@
{
"name": "rax",
- "version": "1.1.4",
+ "version": "1.2.0",
"description": "A universal React-compatible render engine.",
"license": "BSD-3-Clause",
"main": "index.js",
diff --git a/packages/rax/src/vdom/__tests__/composite.js b/packages/rax/src/vdom/__tests__/composite.js
index 50feb4672e..992b78c7de 100644
--- a/packages/rax/src/vdom/__tests__/composite.js
+++ b/packages/rax/src/vdom/__tests__/composite.js
@@ -690,6 +690,78 @@ describe('CompositeComponent', function() {
expect(container.childNodes[0].childNodes[0].data).toBe('Caught an error: Hello.');
});
+ it('should update state to the next render when catch error.', () => {
+ let container = createNodeElement('div');
+ class ErrorBoundary extends Component {
+ constructor(props) {
+ super(props);
+ this.state = { hasError: false };
+ }
+
+ static getDerivedStateFromError(error) {
+ return { hasError: true };
+ }
+
+ componentDidCatch(error, errorInfo) {
+ // log
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return Something went wrong.
;
+ }
+
+ return this.props.children;
+ }
+ }
+
+ function BrokenRender(props) {
+ throw new Error('Hello');
+ }
+
+ render(
+
+
+ , container);
+
+ jest.runAllTimers();
+ expect(container.childNodes[0].childNodes[0].data).toBe('Something went wrong.');
+ });
+
+ it('should catch error only with getDerivedStateFromError.', () => {
+ let container = createNodeElement('div');
+ class ErrorBoundary extends Component {
+ constructor(props) {
+ super(props);
+ this.state = { hasError: false };
+ }
+
+ static getDerivedStateFromError(error) {
+ return { hasError: true };
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return Something went wrong.
;
+ }
+
+ return this.props.children;
+ }
+ }
+
+ function BrokenRender(props) {
+ throw new Error('Hello');
+ }
+
+ render(
+
+
+ , container);
+
+ jest.runAllTimers();
+ expect(container.childNodes[0].childNodes[0].data).toBe('Something went wrong.');
+ });
+
it('should render correct when prevRenderedComponent did not generate nodes', () => {
let container = createNodeElement('div');
class Frag extends Component {
diff --git a/packages/rax/src/vdom/performInSandbox.js b/packages/rax/src/vdom/performInSandbox.js
index 6ec73fafb1..37b574eb6b 100644
--- a/packages/rax/src/vdom/performInSandbox.js
+++ b/packages/rax/src/vdom/performInSandbox.js
@@ -14,8 +14,18 @@ export default function performInSandbox(fn, instance, callback) {
}
}
+/**
+ * A class component becomes an error boundary if
+ * it defines either (or both) of the lifecycle methods static getDerivedStateFromError() or componentDidCatch().
+ * Use static getDerivedStateFromError() to render a fallback UI after an error has been thrown.
+ * Use componentDidCatch() to log error information.
+ * @param {*} instance
+ * @param {*} error
+ */
export function handleError(instance, error) {
- let boundary = getNearestParent(instance, parent => parent.componentDidCatch);
+ let boundary = getNearestParent(instance, parent => {
+ return parent.componentDidCatch || (parent.constructor && parent.constructor.getDerivedStateFromError);
+ });
if (boundary) {
scheduleLayout(() => {
@@ -23,7 +33,15 @@ export function handleError(instance, error) {
// Should not attempt to recover an unmounting error boundary
if (boundaryInternal) {
performInSandbox(() => {
- boundary.componentDidCatch(error);
+ if (boundary.componentDidCatch) {
+ boundary.componentDidCatch(error);
+ }
+
+ // Update state to the next render to show the fallback UI.
+ if (boundary.constructor && boundary.constructor.getDerivedStateFromError) {
+ const state = boundary.constructor.getDerivedStateFromError();
+ boundary.setState(state);
+ }
}, boundaryInternal.__parentInstance);
}
});
@@ -33,4 +51,4 @@ export function handleError(instance, error) {
throw error;
}, 0);
}
-}
\ No newline at end of file
+}