fix(root-inject): handle errors in functional components properly
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import React, { Component } from "react"
|
import React, { Component } from "react"
|
||||||
|
import PropTypes from "prop-types"
|
||||||
import ReactDOM from "react-dom"
|
import ReactDOM from "react-dom"
|
||||||
import { connect, Provider } from "react-redux"
|
import { connect, Provider } from "react-redux"
|
||||||
import omit from "lodash/omit"
|
import omit from "lodash/omit"
|
||||||
@@ -69,30 +70,71 @@ export const render = (getSystem, getStore, getComponent, getComponents, domNode
|
|||||||
ReactDOM.render(( <App/> ), domNode)
|
ReactDOM.render(( <App/> ), domNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ErrorBoundary extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = { hasError: false, error: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error) {
|
||||||
|
return { hasError: true, error }
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error, errorInfo) {
|
||||||
|
console.error(error, errorInfo) // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return <Fallback name={this.props.targetName} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ErrorBoundary.propTypes = {
|
||||||
|
targetName: PropTypes.string,
|
||||||
|
children: PropTypes.oneOfType([
|
||||||
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
|
PropTypes.node,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
ErrorBoundary.defaultProps = {
|
||||||
|
targetName: "this component",
|
||||||
|
children: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Fallback = ({ name }) => (
|
||||||
|
<div className="fallback">
|
||||||
|
😱 <i>Could not render { name === "t" ? "this component" : name }, see the console.</i>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
Fallback.propTypes = {
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
// Render try/catch wrapper
|
// Render try/catch wrapper
|
||||||
const createClass = OriginalComponent => class extends Component {
|
const createClass = OriginalComponent => class extends Component {
|
||||||
render() {
|
render() {
|
||||||
return <OriginalComponent {...this.props} />
|
return (
|
||||||
|
<ErrorBoundary targetName={OriginalComponent?.name}>
|
||||||
|
<OriginalComponent {...this.props} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Fallback = ({
|
|
||||||
name // eslint-disable-line react/prop-types
|
|
||||||
}) => <div className="fallback">😱 <i>Could not render { name === "t" ? "this component" : name }, see the console.</i></div>
|
|
||||||
|
|
||||||
const wrapRender = (component) => {
|
const wrapRender = (component) => {
|
||||||
const isStateless = component => !(component.prototype && component.prototype.isReactComponent)
|
const isStateless = component => !(component.prototype && component.prototype.isReactComponent)
|
||||||
|
|
||||||
const target = isStateless(component) ? createClass(component) : component
|
const target = isStateless(component) ? createClass(component) : component
|
||||||
|
const { render: oriRender} = target.prototype
|
||||||
const ori = target.prototype.render
|
|
||||||
|
|
||||||
target.prototype.render = function render(...args) {
|
target.prototype.render = function render(...args) {
|
||||||
try {
|
try {
|
||||||
return ori.apply(this, args)
|
return oriRender.apply(this, args)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error) // eslint-disable-line no-console
|
console.error(error) // eslint-disable-line no-console
|
||||||
return <Fallback error={error} name={target.name} />
|
return <Fallback name={target.name} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
|
|
||||||
import React, { PureComponent } from "react"
|
import React, { PureComponent } from "react"
|
||||||
|
import { fromJS } from "immutable"
|
||||||
|
import { render, mount } from "enzyme"
|
||||||
|
import { Provider } from "react-redux"
|
||||||
|
|
||||||
import System from "core/system"
|
import System from "core/system"
|
||||||
import { fromJS } from "immutable"
|
|
||||||
import { render } from "enzyme"
|
|
||||||
import ViewPlugin from "core/plugins/view/index.js"
|
import ViewPlugin from "core/plugins/view/index.js"
|
||||||
import filterPlugin from "core/plugins/filter/index.js"
|
import filterPlugin from "core/plugins/filter/index.js"
|
||||||
import { connect, Provider } from "react-redux"
|
|
||||||
|
|
||||||
describe("bound system", function(){
|
describe("bound system", function(){
|
||||||
|
|
||||||
@@ -704,7 +703,7 @@ describe("bound system", function(){
|
|||||||
describe("rootInjects", function() {
|
describe("rootInjects", function() {
|
||||||
it("should attach a rootInject function as an instance method", function() {
|
it("should attach a rootInject function as an instance method", function() {
|
||||||
// This is the same thing as the `afterLoad` tests, but is here for posterity
|
// This is the same thing as the `afterLoad` tests, but is here for posterity
|
||||||
|
|
||||||
// Given
|
// Given
|
||||||
const system = new System({
|
const system = new System({
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -985,10 +984,9 @@ describe("bound system", function(){
|
|||||||
})
|
})
|
||||||
|
|
||||||
// When
|
// When
|
||||||
let Component = system.getSystem().getComponent("BrokenComponent")
|
const Component = system.getSystem().getComponent("BrokenComponent")
|
||||||
const renderedComponent = render(<Component />)
|
const renderedComponent = mount(<Component />)
|
||||||
|
|
||||||
// Then
|
|
||||||
expect(renderedComponent.text().startsWith("😱 Could not render")).toEqual(true)
|
expect(renderedComponent.text().startsWith("😱 Could not render")).toEqual(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user