fix(root-inject): handle errors in functional components properly

This commit is contained in:
Vladimir Gorej
2021-10-14 15:15:42 +03:00
parent 46b4e5cf3f
commit e3640739a4
2 changed files with 58 additions and 18 deletions

View File

@@ -1,4 +1,5 @@
import React, { Component } from "react"
import PropTypes from "prop-types"
import ReactDOM from "react-dom"
import { connect, Provider } from "react-redux"
import omit from "lodash/omit"
@@ -69,30 +70,71 @@ export const render = (getSystem, getStore, getComponent, getComponents, 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
const createClass = OriginalComponent => class extends Component {
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 isStateless = component => !(component.prototype && component.prototype.isReactComponent)
const target = isStateless(component) ? createClass(component) : component
const ori = target.prototype.render
const { render: oriRender} = target.prototype
target.prototype.render = function render(...args) {
try {
return ori.apply(this, args)
return oriRender.apply(this, args)
} catch (error) {
console.error(error) // eslint-disable-line no-console
return <Fallback error={error} name={target.name} />
return <Fallback name={target.name} />
}
}

View File

@@ -1,12 +1,11 @@
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 { fromJS } from "immutable"
import { render } from "enzyme"
import ViewPlugin from "core/plugins/view/index.js"
import filterPlugin from "core/plugins/filter/index.js"
import { connect, Provider } from "react-redux"
describe("bound system", function(){
@@ -704,7 +703,7 @@ describe("bound system", function(){
describe("rootInjects", 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
// Given
const system = new System({
plugins: [
@@ -985,10 +984,9 @@ describe("bound system", function(){
})
// When
let Component = system.getSystem().getComponent("BrokenComponent")
const renderedComponent = render(<Component />)
const Component = system.getSystem().getComponent("BrokenComponent")
const renderedComponent = mount(<Component />)
// Then
expect(renderedComponent.text().startsWith("😱 Could not render")).toEqual(true)
})