Skip to content

Commit 5b7ee93

Browse files
authored
feat: support catch error in SSR (#2041)
* feat: support catch error when ssr * fix: lint * test: catch error * v1.2.2 * test: get derived state from error * test: componentDidCatch * feat: support catch error only with getDerivedStateFromError * fix: call componentDidCatch of instance * fix: check constructor is exist * v1.3.0 * v1.2.0
1 parent ab8db5b commit 5b7ee93

File tree

6 files changed

+179
-8
lines changed

6 files changed

+179
-8
lines changed

packages/rax-server-renderer/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "rax-server-renderer",
3-
"version": "1.2.1",
3+
"version": "1.3.0",
44
"description": "Rax renderer for server-side render.",
55
"license": "BSD-3-Clause",
66
"main": "lib/index.js",

packages/rax-server-renderer/src/__tests__/renderToString.js

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* @jsx createElement */
22

3-
import {createElement, useState, useEffect, createContext, useContext, useReducer} from 'rax';
3+
import {createElement, Component, useState, useEffect, createContext, useContext, useReducer} from 'rax';
44
import {renderToString} from '../index';
55

66
describe('renderToString', () => {
@@ -353,4 +353,72 @@ describe('renderToString', () => {
353353
const str = renderToString(<App />);
354354
expect(str).toBe('<!-- _ --><div>light</div>');
355355
});
356+
357+
it('should catch error with componentDidCatch', function() {
358+
class ErrorBoundary extends Component {
359+
constructor(props) {
360+
super(props);
361+
}
362+
363+
componentDidCatch(error, errorInfo) {
364+
// log error
365+
}
366+
367+
render() {
368+
return this.props.children;
369+
}
370+
}
371+
372+
function MyWidget() {
373+
throw new Error('widget error');
374+
}
375+
376+
function App() {
377+
return (
378+
<div>
379+
<ErrorBoundary>
380+
<MyWidget />
381+
</ErrorBoundary>
382+
</div>
383+
);
384+
};
385+
386+
const str = renderToString(<App />);
387+
expect(str).toBe('<div><!--ERROR--></div>');
388+
});
389+
390+
it('should call componentDidCatch when catch error', function() {
391+
const mockFn = jest.fn();
392+
class ErrorBoundary extends Component {
393+
constructor(props) {
394+
super(props);
395+
}
396+
397+
componentDidCatch(error, errorInfo) {
398+
mockFn();
399+
}
400+
401+
render() {
402+
return this.props.children;
403+
}
404+
}
405+
406+
function MyWidget() {
407+
throw new Error('widget error');
408+
}
409+
410+
function App() {
411+
return (
412+
<div>
413+
<ErrorBoundary>
414+
<MyWidget />
415+
</ErrorBoundary>
416+
</div>
417+
);
418+
};
419+
420+
const str = renderToString(<App />);
421+
expect(mockFn).toHaveBeenCalled();
422+
expect(str).toBe('<div><!--ERROR--></div>');
423+
});
356424
});

packages/rax-server-renderer/src/index.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const VOID_ELEMENTS = {
2222
};
2323

2424
const TEXT_SPLIT_COMMENT = '<!--|-->';
25+
const ERROR_COMMENT = '<!--ERROR-->';
2526

2627
const ESCAPE_LOOKUP = {
2728
'&': '&amp;',
@@ -357,8 +358,11 @@ class ServerRenderer {
357358
const type = element.type;
358359

359360
if (type) {
361+
const isClassComponent = type.prototype && type.prototype.render;
362+
const isFunctionComponent = typeof type === 'function';
363+
360364
// class component || function component
361-
if (type.prototype && type.prototype.render || typeof type === 'function') {
365+
if (isClassComponent || isFunctionComponent) {
362366
const instance = createInstance(element, context);
363367

364368
const currentComponent = {
@@ -399,7 +403,16 @@ class ServerRenderer {
399403
// Reset owner after render, or it will casue memory leak.
400404
shared.Host.owner = null;
401405

402-
return this.renderElementToString(renderedElement, currentContext);
406+
if (isClassComponent && instance.componentDidCatch) {
407+
try {
408+
return this.renderElementToString(renderedElement, currentContext);
409+
} catch(e) {
410+
instance.componentDidCatch(e);
411+
return ERROR_COMMENT;
412+
}
413+
} else {
414+
return this.renderElementToString(renderedElement, currentContext);
415+
}
403416
} else if (typeof type === 'string') {
404417
// shoud set the identifier to false before render child
405418
this.previousWasTextNode = false;

packages/rax/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "rax",
3-
"version": "1.1.4",
3+
"version": "1.2.0",
44
"description": "A universal React-compatible render engine.",
55
"license": "BSD-3-Clause",
66
"main": "index.js",

packages/rax/src/vdom/__tests__/composite.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,78 @@ describe('CompositeComponent', function() {
690690
expect(container.childNodes[0].childNodes[0].data).toBe('Caught an error: Hello.');
691691
});
692692

693+
it('should update state to the next render when catch error.', () => {
694+
let container = createNodeElement('div');
695+
class ErrorBoundary extends Component {
696+
constructor(props) {
697+
super(props);
698+
this.state = { hasError: false };
699+
}
700+
701+
static getDerivedStateFromError(error) {
702+
return { hasError: true };
703+
}
704+
705+
componentDidCatch(error, errorInfo) {
706+
// log
707+
}
708+
709+
render() {
710+
if (this.state.hasError) {
711+
return <h1>Something went wrong.</h1>;
712+
}
713+
714+
return this.props.children;
715+
}
716+
}
717+
718+
function BrokenRender(props) {
719+
throw new Error('Hello');
720+
}
721+
722+
render(
723+
<ErrorBoundary>
724+
<BrokenRender />
725+
</ErrorBoundary>, container);
726+
727+
jest.runAllTimers();
728+
expect(container.childNodes[0].childNodes[0].data).toBe('Something went wrong.');
729+
});
730+
731+
it('should catch error only with getDerivedStateFromError.', () => {
732+
let container = createNodeElement('div');
733+
class ErrorBoundary extends Component {
734+
constructor(props) {
735+
super(props);
736+
this.state = { hasError: false };
737+
}
738+
739+
static getDerivedStateFromError(error) {
740+
return { hasError: true };
741+
}
742+
743+
render() {
744+
if (this.state.hasError) {
745+
return <h1>Something went wrong.</h1>;
746+
}
747+
748+
return this.props.children;
749+
}
750+
}
751+
752+
function BrokenRender(props) {
753+
throw new Error('Hello');
754+
}
755+
756+
render(
757+
<ErrorBoundary>
758+
<BrokenRender />
759+
</ErrorBoundary>, container);
760+
761+
jest.runAllTimers();
762+
expect(container.childNodes[0].childNodes[0].data).toBe('Something went wrong.');
763+
});
764+
693765
it('should render correct when prevRenderedComponent did not generate nodes', () => {
694766
let container = createNodeElement('div');
695767
class Frag extends Component {

packages/rax/src/vdom/performInSandbox.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,34 @@ export default function performInSandbox(fn, instance, callback) {
1414
}
1515
}
1616

17+
/**
18+
* A class component becomes an error boundary if
19+
* it defines either (or both) of the lifecycle methods static getDerivedStateFromError() or componentDidCatch().
20+
* Use static getDerivedStateFromError() to render a fallback UI after an error has been thrown.
21+
* Use componentDidCatch() to log error information.
22+
* @param {*} instance
23+
* @param {*} error
24+
*/
1725
export function handleError(instance, error) {
18-
let boundary = getNearestParent(instance, parent => parent.componentDidCatch);
26+
let boundary = getNearestParent(instance, parent => {
27+
return parent.componentDidCatch || (parent.constructor && parent.constructor.getDerivedStateFromError);
28+
});
1929

2030
if (boundary) {
2131
scheduleLayout(() => {
2232
const boundaryInternal = boundary[INTERNAL];
2333
// Should not attempt to recover an unmounting error boundary
2434
if (boundaryInternal) {
2535
performInSandbox(() => {
26-
boundary.componentDidCatch(error);
36+
if (boundary.componentDidCatch) {
37+
boundary.componentDidCatch(error);
38+
}
39+
40+
// Update state to the next render to show the fallback UI.
41+
if (boundary.constructor && boundary.constructor.getDerivedStateFromError) {
42+
const state = boundary.constructor.getDerivedStateFromError();
43+
boundary.setState(state);
44+
}
2745
}, boundaryInternal.__parentInstance);
2846
}
2947
});
@@ -33,4 +51,4 @@ export function handleError(instance, error) {
3351
throw error;
3452
}, 0);
3553
}
36-
}
54+
}

0 commit comments

Comments
 (0)