Repository: streamich/react-use
Pull request #902 (2020-01-16 15:45:57)

feat: add useError hook
# Description

<!-- Please include a summary of the change along with relevant motivation and context. -->
#64 


## Type of change

<!-- Check all relevant options. -->
- [ ] Bug fix _(non-breaking change which fixes an issue)_
- [x] New feature _(non-breaking change which adds functionality)_
- [ ] **Breaking change** _(fix or feature that would cause existing functionality to not work as before)_

# Checklist
- [x] Read the [Contributing Guide](https://github.com/streamich/react-use/blob/master/CONTRIBUTING.md)
- [x] Perform a code self-review
- [x] Comment the code, particularly in hard-to-understand areas
- [x] Add documentation
- [x] Add hook's story at Storybook
- [x] Cover changes with tests
- [x] Ensure the test suite passes (`yarn test`)
- [x] Provide 100% tests coverage
- [x] Make sure code lints (`yarn lint`). Fix it with `yarn lint:fix` in case of failure.
- [x] Make sure types are fine (`yarn lint:types`).

<!-- If you can't check all the checkboxes right now - check what you can, create a Draft PR, make some changes if needed and get back to it when you will be able to put some marks in list. -->



Patch:
diff --git a/README.md b/README.md
index 3d3603c4..6138cc28 100644
--- a/README.md
+++ b/README.md
@@ -97,6 +97,7 @@
   - [`useCookie`](./docs/useCookie.md) &mdash; provides way to read, update and delete a cookie. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/side-effects-usecookie--demo)
   - [`useCopyToClipboard`](./docs/useCopyToClipboard.md) &mdash; copies text to clipboard.
   - [`useDebounce`](./docs/useDebounce.md) &mdash; debounces a function. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/side-effects-usedebounce--demo)
+  - [`useError`](./docs/useError.md) &mdash; error dispatcher. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/side-effects-useerror--demo)
   - [`useFavicon`](./docs/useFavicon.md) &mdash; sets favicon of the page.
   - [`useLocalStorage`](./docs/useLocalStorage.md) &mdash; manages a value in `localStorage`.
   - [`useLockBodyScroll`](./docs/useLockBodyScroll.md) &mdash; lock scrolling of the body element.
diff --git a/docs/useError.md b/docs/useError.md
new file mode 100644
index 00000000..5a506f6d
--- /dev/null
+++ b/docs/useError.md
@@ -0,0 +1,34 @@
+# `useError`
+
+React side-effect hook that returns an error dispatcher.
+
+## Usage
+
+```jsx
+import { useError } from 'react-use';
+
+const Demo = () => {
+  const dispatchError = useError();
+
+  const clickHandler = () => {
+    dispatchError(new Error('Some error!'));
+  };
+
+  return <button onClick={clickHandler}>Click me to throw</button>;
+};
+
+// In parent app
+const App = () => (
+  <ErrorBoundary>
+    <Demo />
+  </ErrorBoundary>
+);
+```
+
+## Reference
+
+```js
+const dispatchError = useError();
+```
+
+- `dispatchError` &mdash; Callback of type `(err: Error) => void`
diff --git a/src/index.ts b/src/index.ts
index dc6f65ff..d4730ca7 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -21,6 +21,7 @@ export { default as useDropArea } from './useDropArea';
 export { default as useEffectOnce } from './useEffectOnce';
 export { default as useEnsuredForwardedRef, ensuredForwardRef } from './useEnsuredForwardedRef';
 export { default as useEvent } from './useEvent';
+export { default as useError } from './useError';
 export { default as useFavicon } from './useFavicon';
 export { default as useFullscreen } from './useFullscreen';
 export { default as useGeolocation } from './useGeolocation';
diff --git a/src/useError.ts b/src/useError.ts
new file mode 100644
index 00000000..b437901f
--- /dev/null
+++ b/src/useError.ts
@@ -0,0 +1,19 @@
+import { useState, useEffect, useCallback } from 'react';
+
+const useError = (): ((err: Error) => void) => {
+  const [error, setError] = useState<Error | null>(null);
+
+  useEffect(() => {
+    if (error) {
+      throw error;
+    }
+  }, [error]);
+
+  const dispatchError = useCallback((err: Error) => {
+    setError(err);
+  }, []);
+
+  return dispatchError;
+};
+
+export default useError;
diff --git a/stories/useError.story.tsx b/stories/useError.story.tsx
new file mode 100644
index 00000000..e42c8052
--- /dev/null
+++ b/stories/useError.story.tsx
@@ -0,0 +1,46 @@
+import { storiesOf } from '@storybook/react';
+import React from 'react';
+import { useError } from '../src';
+import ShowDocs from './util/ShowDocs';
+
+class ErrorBoundary extends React.Component<{}, { hasError: boolean }> {
+  constructor(props) {
+    super(props);
+    this.state = { hasError: false };
+  }
+
+  static getDerivedStateFromError(error) {
+    return { hasError: true };
+  }
+
+  render() {
+    if (this.state.hasError) {
+      return (
+        <div>
+          <h1>Something went wrong.</h1>
+          <button onClick={() => this.setState({ hasError: false })}>Retry</button>
+        </div>
+      );
+    }
+
+    return this.props.children;
+  }
+}
+
+const Demo = () => {
+  const dispatchError = useError();
+
+  const clickHandler = () => {
+    dispatchError(new Error('Some error!'));
+  };
+
+  return <button onClick={clickHandler}>Click me to throw</button>;
+};
+
+storiesOf('Side effects|useError', module)
+  .add('Docs', () => <ShowDocs md={require('../docs/useLocalStorage.md')} />)
+  .add('Demo', () => (
+    <ErrorBoundary>
+      <Demo />
+    </ErrorBoundary>
+  ));


Test patch:
diff --git a/tests/useError.test.ts b/tests/useError.test.ts
new file mode 100644
index 00000000..2e3af244
--- /dev/null
+++ b/tests/useError.test.ts
@@ -0,0 +1,26 @@
+import { renderHook, act } from '@testing-library/react-hooks';
+import { useError } from '../src';
+
+const setup = () => renderHook(() => useError());
+
+beforeEach(() => {
+  jest.spyOn(console, 'error').mockImplementation(() => {});
+});
+
+afterEach(() => {
+  jest.clearAllMocks();
+});
+
+it('should throw an error on error dispatch', () => {
+  const errorStr = 'some_error';
+
+  try {
+    const { result } = setup();
+
+    act(() => {
+      result.current(new Error(errorStr));
+    });
+  } catch (err) {
+    expect(err.message).toEqual(errorStr);
+  }
+});