Skip to content

Commit

Permalink
fix(root-inject): handle errors in functional components properly
Browse files Browse the repository at this point in the history
  • Loading branch information
char0n committed Nov 3, 2021
1 parent 46b4e5c commit e364073
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 18 deletions.
62 changes: 52 additions & 10 deletions src/core/plugins/view/root-injects.jsx
@@ -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"
Expand Down Expand Up @@ -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} />
}
}

Expand Down
14 changes: 6 additions & 8 deletions test/unit/core/system/system.jsx
@@ -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(){

Expand Down Expand Up @@ -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: [
Expand Down Expand Up @@ -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)
})

Expand Down

0 comments on commit e364073

Please sign in to comment.