Skip to content

Commit b299be7

Browse files
authored
fix: introduce Error Boundaries to handle unexpected failures (#7671)
Two new components have been updated via plugin system: ErrorBoundary and Fallback. These components can be overridden by user plugins. Refs #7647
1 parent fd22564 commit b299be7

File tree

8 files changed

+157
-89
lines changed

8 files changed

+157
-89
lines changed

config/jest/jest.unit.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module.exports = {
1818
'<rootDir>/test/unit/components/online-validator-badge.jsx',
1919
'<rootDir>/test/unit/components/live-response.jsx',
2020
],
21+
silent: true, // set to `false` to allow console.* calls to be printed
2122
transformIgnorePatterns: [
2223
'/node_modules/(?!(react-syntax-highlighter)/)'
2324
]

src/core/components/layouts/base.jsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export default class BaseLayout extends React.Component {
2828
const SchemesContainer = getComponent("SchemesContainer", true)
2929
const AuthorizeBtnContainer = getComponent("AuthorizeBtnContainer", true)
3030
const FilterContainer = getComponent("FilterContainer", true)
31+
const ErrorBoundary = getComponent("ErrorBoundary", true)
3132
let isSwagger2 = specSelectors.isSwagger2()
3233
let isOAS3 = specSelectors.isOAS3()
3334

@@ -36,7 +37,7 @@ export default class BaseLayout extends React.Component {
3637
const loadingStatus = specSelectors.loadingStatus()
3738

3839
let loadingMessage = null
39-
40+
4041
if(loadingStatus === "loading") {
4142
loadingMessage = <div className="info">
4243
<div className="loading-container">
@@ -85,8 +86,8 @@ export default class BaseLayout extends React.Component {
8586
const hasSecurityDefinitions = !!specSelectors.securityDefinitions()
8687

8788
return (
88-
8989
<div className='swagger-ui'>
90+
<ErrorBoundary targetName="BaseLayout">
9091
<SvgAssets />
9192
<VersionPragmaFilter isSwagger2={isSwagger2} isOAS3={isOAS3} alsoShow={<Errors/>}>
9293
<Errors/>
@@ -119,7 +120,8 @@ export default class BaseLayout extends React.Component {
119120
</Col>
120121
</Row>
121122
</VersionPragmaFilter>
122-
</div>
123-
)
123+
</ErrorBoundary>
124+
</div>
125+
)
124126
}
125127
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import PropTypes from "prop-types"
2+
import React, { Component } from "react"
3+
4+
import Fallback from "./fallback"
5+
6+
export class ErrorBoundary extends Component {
7+
constructor(props) {
8+
super(props)
9+
this.state = { hasError: false, error: null }
10+
}
11+
12+
static getDerivedStateFromError(error) {
13+
return { hasError: true, error }
14+
}
15+
16+
componentDidCatch(error, errorInfo) {
17+
console.error(error, errorInfo) // eslint-disable-line no-console
18+
}
19+
20+
render() {
21+
const { getComponent, targetName, children } = this.props
22+
const FallbackComponent = getComponent("Fallback")
23+
24+
if (this.state.hasError) {
25+
return <FallbackComponent name={targetName} />
26+
}
27+
28+
return children
29+
}
30+
}
31+
ErrorBoundary.propTypes = {
32+
targetName: PropTypes.string,
33+
getComponent: PropTypes.func,
34+
children: PropTypes.oneOfType([
35+
PropTypes.arrayOf(PropTypes.node),
36+
PropTypes.node,
37+
])
38+
}
39+
ErrorBoundary.defaultProps = {
40+
targetName: "this component",
41+
getComponent: () => Fallback,
42+
children: null,
43+
}
44+
45+
export default ErrorBoundary

src/core/plugins/view/fallback.jsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from "react"
2+
import PropTypes from "prop-types"
3+
4+
const Fallback = ({ name }) => (
5+
<div className="fallback">
6+
😱 <i>Could not render { name === "t" ? "this component" : name }, see the console.</i>
7+
</div>
8+
)
9+
Fallback.propTypes = {
10+
name: PropTypes.string.isRequired,
11+
}
12+
13+
export default Fallback

src/core/plugins/view/index.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import * as rootInjects from "./root-injects"
22
import { memoize } from "core/utils"
33

4+
import ErrorBoundary from "./error-boundary"
5+
import Fallback from "./fallback"
6+
47
export default function({getComponents, getStore, getSystem}) {
58

69
let { getComponent, render, makeMappedContainer } = rootInjects
@@ -14,6 +17,10 @@ export default function({getComponents, getStore, getSystem}) {
1417
getComponent: memGetComponent,
1518
makeMappedContainer: memMakeMappedContainer,
1619
render: render.bind(null, getSystem, getStore, getComponent, getComponents),
17-
}
20+
},
21+
components: {
22+
ErrorBoundary,
23+
Fallback,
24+
},
1825
}
1926
}
Lines changed: 30 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
import React, { Component } from "react"
2-
import PropTypes from "prop-types"
32
import ReactDOM from "react-dom"
43
import { connect, Provider } from "react-redux"
54
import omit from "lodash/omit"
65

76
const SystemWrapper = (getSystem, ComponentToWrap ) => class extends Component {
87
render() {
9-
return <ComponentToWrap {...getSystem() } {...this.props} {...this.context} />
8+
return <ComponentToWrap {...getSystem()} {...this.props} {...this.context} />
109
}
1110
}
1211

13-
const RootWrapper = (reduxStore, ComponentToWrap) => class extends Component {
12+
const RootWrapper = (getSystem, reduxStore, ComponentToWrap) => class extends Component {
1413
render() {
14+
const { getComponent } = getSystem()
15+
const ErrorBoundary = getComponent("ErrorBoundary", true)
16+
1517
return (
1618
<Provider store={reduxStore}>
17-
<ComponentToWrap {...this.props} {...this.context} />
19+
<ErrorBoundary targetName={ComponentToWrap?.name}>
20+
<ComponentToWrap {...this.props} {...this.context} />
21+
</ErrorBoundary>
1822
</Provider>
1923
)
2024
}
@@ -30,7 +34,7 @@ const makeContainer = (getSystem, component, reduxStore) => {
3034
let wrappedWithSystem = SystemWrapper(getSystem, component, reduxStore)
3135
let connected = connect( mapStateToProps )(wrappedWithSystem)
3236
if(reduxStore)
33-
return RootWrapper(reduxStore, connected)
37+
return RootWrapper(getSystem, reduxStore, connected)
3438
return connected
3539
}
3640

@@ -66,73 +70,43 @@ export const makeMappedContainer = (getSystem, getStore, memGetComponent, getCom
6670
}
6771

6872
export const render = (getSystem, getStore, getComponent, getComponents, domNode) => {
69-
let App = (getComponent(getSystem, getStore, getComponents, "App", "root"))
70-
ReactDOM.render(( <App/> ), domNode)
73+
let App = getComponent(getSystem, getStore, getComponents, "App", "root")
74+
ReactDOM.render(<App/>, domNode)
7175
}
7276

73-
class ErrorBoundary extends Component {
74-
constructor(props) {
75-
super(props)
76-
this.state = { hasError: false, error: null }
77-
}
78-
79-
static getDerivedStateFromError(error) {
80-
return { hasError: true, error }
81-
}
82-
83-
componentDidCatch(error, errorInfo) {
84-
console.error(error, errorInfo) // eslint-disable-line no-console
85-
}
86-
77+
/**
78+
* Creates a class component from a stateless one and wrap it with Error Boundary
79+
* to handle errors coming from a stateless component.
80+
*/
81+
const createClass = (getSystem, OriginalComponent) => class extends Component {
8782
render() {
88-
if (this.state.hasError) {
89-
return <Fallback name={this.props.targetName} />
90-
}
83+
const { getComponent } = getSystem()
84+
const ErrorBoundary = getComponent("ErrorBoundary")
9185

92-
return this.props.children
93-
}
94-
}
95-
ErrorBoundary.propTypes = {
96-
targetName: PropTypes.string,
97-
children: PropTypes.oneOfType([
98-
PropTypes.arrayOf(PropTypes.node),
99-
PropTypes.node,
100-
])
101-
}
102-
ErrorBoundary.defaultProps = {
103-
targetName: "this component",
104-
children: null,
105-
}
106-
107-
const Fallback = ({ name }) => (
108-
<div className="fallback">
109-
😱 <i>Could not render { name === "t" ? "this component" : name }, see the console.</i>
110-
</div>
111-
)
112-
Fallback.propTypes = {
113-
name: PropTypes.string.isRequired,
114-
}
115-
116-
// Render try/catch wrapper
117-
const createClass = OriginalComponent => class extends Component {
118-
render() {
11986
return (
120-
<ErrorBoundary targetName={OriginalComponent?.name}>
87+
<ErrorBoundary targetName={OriginalComponent?.name} getComponent={getComponent}>
12188
<OriginalComponent {...this.props} />
12289
</ErrorBoundary>
12390
)
12491
}
12592
}
12693

127-
const wrapRender = (component) => {
94+
const wrapRender = (getSystem, component) => {
12895
const isStateless = component => !(component.prototype && component.prototype.isReactComponent)
129-
const target = isStateless(component) ? createClass(component) : component
96+
const target = isStateless(component) ? createClass(getSystem, component) : component
13097
const { render: oriRender} = target.prototype
13198

99+
/**
100+
* This render method override handles errors that are throw in render method
101+
* of class components.
102+
*/
132103
target.prototype.render = function render(...args) {
133104
try {
134105
return oriRender.apply(this, args)
135106
} catch (error) {
107+
const { getComponent } = getSystem()
108+
const Fallback = getComponent("Fallback")
109+
136110
console.error(error) // eslint-disable-line no-console
137111
return <Fallback name={target.name} />
138112
}
@@ -159,11 +133,11 @@ export const getComponent = (getSystem, getStore, getComponents, componentName,
159133
}
160134

161135
if(!container)
162-
return wrapRender(component)
136+
return wrapRender(getSystem, component)
163137

164138
if(container === "root")
165139
return makeContainer(getSystem, component, getStore())
166140

167141
// container == truthy
168-
return makeContainer(getSystem, wrapRender(component))
142+
return makeContainer(getSystem, wrapRender(getSystem, component))
169143
}

src/standalone/layout.jsx

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
2-
31
import React from "react"
42
import PropTypes from "prop-types"
53

@@ -16,27 +14,29 @@ export default class StandaloneLayout extends React.Component {
1614
}
1715

1816
render() {
19-
let { getComponent } = this.props
20-
21-
let Container = getComponent("Container")
22-
let Row = getComponent("Row")
23-
let Col = getComponent("Col")
24-
17+
const { getComponent } = this.props
18+
const Container = getComponent("Container")
19+
const Row = getComponent("Row")
20+
const Col = getComponent("Col")
2521
const Topbar = getComponent("Topbar", true)
2622
const BaseLayout = getComponent("BaseLayout", true)
2723
const OnlineValidatorBadge = getComponent("onlineValidatorBadge", true)
24+
const ErrorBoundary = getComponent("ErrorBoundary", true)
2825

2926

3027
return (
31-
3228
<Container className='swagger-ui'>
33-
{Topbar ? <Topbar /> : null}
34-
<BaseLayout />
35-
<Row>
36-
<Col>
37-
<OnlineValidatorBadge />
38-
</Col>
39-
</Row>
29+
<ErrorBoundary targetName="Topbar">
30+
{Topbar ? <Topbar /> : null}
31+
</ErrorBoundary>
32+
<BaseLayout />
33+
<ErrorBoundary targetName="OnlineValidatorBadge">
34+
<Row>
35+
<Col>
36+
<OnlineValidatorBadge />
37+
</Col>
38+
</Row>
39+
</ErrorBoundary>
4040
</Container>
4141
)
4242
}

0 commit comments

Comments
 (0)