-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(react): add ErrorBoundary component (#4619)
* feat(react): add ErrorBoundary component * docs(react): add story for ErrorBoundary * feat(react): add ErrorBoundary to public api
- Loading branch information
Showing
7 changed files
with
297 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
82 changes: 82 additions & 0 deletions
82
packages/react/src/components/ErrorBoundary/ErrorBoundary-story.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
/** | ||
* Copyright IBM Corp. 2016, 2018 | ||
* | ||
* This source code is licensed under the Apache-2.0 license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
|
||
import React, { useState } from 'react'; | ||
import { storiesOf } from '@storybook/react'; | ||
import { action } from '@storybook/addon-actions'; | ||
import { ErrorBoundary, ErrorBoundaryContext } from './'; | ||
import Button from '../Button'; | ||
|
||
storiesOf('ErrorBoundary', module) | ||
.add('default', () => { | ||
function DemoComponent() { | ||
const [shouldThrowError, setShouldThrowError] = useState(false); | ||
|
||
function onClick() { | ||
setShouldThrowError(!shouldThrowError); | ||
} | ||
|
||
return ( | ||
<> | ||
<Button onClick={onClick}>Toggle throwing error</Button> | ||
<div> | ||
<ErrorBoundary fallback={<Fallback />}> | ||
<ThrowError shouldThrowError={shouldThrowError} /> | ||
</ErrorBoundary> | ||
</div> | ||
</> | ||
); | ||
} | ||
|
||
function Fallback() { | ||
return 'Whoops'; | ||
} | ||
|
||
function ThrowError({ shouldThrowError }) { | ||
if (shouldThrowError) { | ||
throw new Error('Component threw error'); | ||
} | ||
|
||
return 'Successfully rendered'; | ||
} | ||
|
||
return <DemoComponent />; | ||
}) | ||
.add('with custom context', () => { | ||
function DemoComponent() { | ||
const [shouldThrowError, setShouldThrowError] = useState(false); | ||
|
||
function onClick() { | ||
setShouldThrowError(!shouldThrowError); | ||
} | ||
|
||
return ( | ||
<ErrorBoundaryContext.Provider value={{ log: action('log') }}> | ||
<Button onClick={onClick}>Toggle throwing error</Button> | ||
<div> | ||
<ErrorBoundary fallback={<Fallback />}> | ||
<ThrowError shouldThrowError={shouldThrowError} /> | ||
</ErrorBoundary> | ||
</div> | ||
</ErrorBoundaryContext.Provider> | ||
); | ||
} | ||
|
||
function Fallback() { | ||
return 'Whoops'; | ||
} | ||
|
||
function ThrowError({ shouldThrowError }) { | ||
if (shouldThrowError) { | ||
throw new Error('Component threw error'); | ||
} | ||
|
||
return 'Successfully rendered'; | ||
} | ||
|
||
return <DemoComponent />; | ||
}); |
63 changes: 63 additions & 0 deletions
63
packages/react/src/components/ErrorBoundary/ErrorBoundary.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
/** | ||
* Copyright IBM Corp. 2016, 2018 | ||
* | ||
* This source code is licensed under the Apache-2.0 license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
|
||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { ErrorBoundaryContext } from './ErrorBoundaryContext'; | ||
|
||
/** | ||
* React introduced additional lifecycle methods in v16 for capturing errors | ||
* that occur in a specific sub-tree of components. This component helps to | ||
* consolidate some of the duplication that occurs when using these lifecycle | ||
* methods across a codebase. In addition, it allows you to specify the fallback | ||
* UI to display when an error occurs in the sub-tree through the `fallback` | ||
* prop. | ||
* | ||
* This component roughly follows the React.js docs example code for these | ||
* methods. In addition, it takes advantage of an `ErrorBoundaryContext` so that | ||
* consumers can specify their own logic for logging errors. For example, | ||
* reporting an error in the UI to an external service for every `ErrorBoundary` | ||
* used. | ||
* | ||
* Reference: | ||
* https://reactjs.org/docs/error-boundaries.html#introducing-error-boundaries | ||
*/ | ||
export default class ErrorBoundary extends React.Component { | ||
static propTypes = { | ||
children: PropTypes.node, | ||
fallback: PropTypes.node, | ||
}; | ||
|
||
static contextType = ErrorBoundaryContext; | ||
|
||
static getDerivedStateFromError() { | ||
return { | ||
hasError: true, | ||
}; | ||
} | ||
|
||
state = { | ||
hasError: false, | ||
}; | ||
|
||
componentDidCatch(error, info) { | ||
this.context.log(error, info); | ||
} | ||
|
||
componentDidUpdate(prevProps) { | ||
if (prevProps.children !== this.props.children) { | ||
this.setState({ hasError: false }); | ||
} | ||
} | ||
|
||
render() { | ||
if (this.state.hasError) { | ||
return this.props.fallback; | ||
} | ||
return this.props.children; | ||
} | ||
} |
14 changes: 14 additions & 0 deletions
14
packages/react/src/components/ErrorBoundary/ErrorBoundaryContext.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
/** | ||
* Copyright IBM Corp. 2016, 2018 | ||
* | ||
* This source code is licensed under the Apache-2.0 license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
|
||
import { createContext } from 'react'; | ||
|
||
export const ErrorBoundaryContext = createContext({ | ||
log(error, info) { | ||
console.log(info.componentStack); | ||
}, | ||
}); |
123 changes: 123 additions & 0 deletions
123
packages/react/src/components/ErrorBoundary/__tests__/ErrorBoundary-test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
/** | ||
* Copyright IBM Corp. 2016, 2018 | ||
* | ||
* This source code is licensed under the Apache-2.0 license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
|
||
import { render, cleanup } from '@carbon/test-utils/react'; | ||
import React, { useState } from 'react'; | ||
import { ErrorBoundary, ErrorBoundaryContext } from '../'; | ||
|
||
describe('ErrorBoundary', () => { | ||
afterEach(cleanup); | ||
|
||
it('should render children and not a fallback if no error is caught', () => { | ||
function MockComponent() { | ||
return <span data-test-id="mock">mock</span>; | ||
} | ||
|
||
function MockFallback() { | ||
return <span data-test-id="fallback">mock</span>; | ||
} | ||
|
||
const { container } = render( | ||
<ErrorBoundary fallback={<MockFallback />}> | ||
<MockComponent /> | ||
</ErrorBoundary> | ||
); | ||
|
||
const component = container.querySelector('[data-test-id="mock"]'); | ||
const fallback = container.querySelector('[data-test-id="fallback"]'); | ||
|
||
expect(component).toBeDefined(); | ||
expect(fallback).toBe(null); | ||
}); | ||
|
||
it('should render a fallback if an error is caught', () => { | ||
console.error = jest.spyOn(console, 'error').mockImplementation(() => {}); | ||
|
||
function MockComponent() { | ||
throw new Error('test error'); | ||
} | ||
|
||
function MockFallback() { | ||
return <span data-test-id="fallback">mock</span>; | ||
} | ||
|
||
const log = jest.fn(); | ||
|
||
const { container } = render( | ||
<ErrorBoundaryContext.Provider value={{ log }}> | ||
<ErrorBoundary fallback={<MockFallback />}> | ||
<MockComponent /> | ||
</ErrorBoundary> | ||
</ErrorBoundaryContext.Provider> | ||
); | ||
|
||
const component = container.querySelector('[data-test-id="mock"]'); | ||
const fallback = container.querySelector('[data-test-id="fallback"]'); | ||
|
||
expect(component).toBe(null); | ||
expect(fallback).toBeDefined(); | ||
expect(console.error).toHaveBeenCalled(); | ||
expect(log).toHaveBeenCalled(); | ||
|
||
console.error.mockRestore(); | ||
}); | ||
|
||
it('should reset from fallback if children have changed', () => { | ||
console.error = jest.spyOn(console, 'error').mockImplementation(() => {}); | ||
|
||
let content = null; | ||
let fallback = null; | ||
let button = null; | ||
|
||
function ThrowError({ shouldThrowError }) { | ||
if (shouldThrowError) { | ||
throw new Error('test error'); | ||
} | ||
return <span ref={element => (content = element)}>mock</span>; | ||
} | ||
|
||
function MockFallback() { | ||
return <span ref={element => (fallback = element)}>mock</span>; | ||
} | ||
|
||
function MockTest() { | ||
const [shouldThrow, setShouldThrow] = useState(false); | ||
|
||
function onClick() { | ||
setShouldThrow(!shouldThrow); | ||
} | ||
|
||
return ( | ||
<ErrorBoundaryContext.Provider value={{ log: jest.fn() }}> | ||
<button ref={element => (button = element)} onClick={onClick}> | ||
Toggle | ||
</button> | ||
<ErrorBoundary fallback={<MockFallback />}> | ||
<ThrowError shouldThrowError={shouldThrow} /> | ||
</ErrorBoundary> | ||
</ErrorBoundaryContext.Provider> | ||
); | ||
} | ||
|
||
render(<MockTest />); | ||
|
||
expect(content).toBeDefined(); | ||
expect(fallback).toBe(null); | ||
|
||
button.click(); | ||
|
||
expect(content).toBe(null); | ||
expect(fallback).toBeDefined(); | ||
|
||
button.click(); | ||
|
||
expect(content).toBeDefined(); | ||
expect(fallback).toBe(null); | ||
|
||
console.error.mockRestore(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
/** | ||
* Copyright IBM Corp. 2016, 2018 | ||
* | ||
* This source code is licensed under the Apache-2.0 license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
|
||
export { default as ErrorBoundary } from './ErrorBoundary'; | ||
export { ErrorBoundaryContext } from './ErrorBoundaryContext'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters