Noob here, not too sure about component configuration, so I ended up with an routing warning error (i.e. "No routes matched location "/play") which is displayed as a warning.
I'm tying to convert this into an error, and then hand it over to ErrorBoundary. It gets converted, but it's not being caught, so ErrorBoundary doesn't activate. What am I doing wrong?
//coverting the warning --No routes matched location "/play"-- into an error
console.warn = function (...args) {
setTimeout(() => {
throw new Error(args[0]);
}, 0);
};
class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
console.error("Error boundary caugh an error", error, info);
}
Convert a warning into an error and handle it with ErrorBoundary
According to react documents
Error boundaries do not catch errors for asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks).
Asynchronous errors can be handled using unhandledrejection event.
import { useEffect } from "react";
export default function ErrorBoundary({ children }) {
const captureReject = (e) => {
e.preventDefault();
console.log(error);
};
useEffect(() => {
window.addEventListener('unhandledrejection', captureReject);
return () => {
window.removeEventListener('unhandledrejection', captureReject);
}
}, []);
return children;
}
You can use it like this.
Related
I have a custom React hook as follows:
export function useValidPolicyReference({
token,
tenant,
policyReference,
}: UseValidPolicyReferenceProps): Record<string, boolean> {
const [isValid, setIsValid] = React.useState<boolean>(true);
React.useEffect(() => {
async function fetchOnMount(): Promise<void> {
try {
await getPolicyByReference({
policyReference: policyReference as string,
tenant,
token,
});
} catch (error) {
setIsValid(false);
}
}
if (tenant !== '' && policyReference !== null && token !== '') {
fetchOnMount();
}
}, [policyReference, tenant, token]);
return {
isValidPolicyReference: isValid,
};
I want to test that isValidPolicyReference will return true when getPolicyByReference does not throw and false when it does.
My test for the true case is as follows:
test('should be truthy if the policy reference is valid', () => {
(getPolicyByReference as jest.Mock).mockImplementationOnce(() =>
Promise.resolve(true)
);
const {
result: { current },
waitForNextUpdate,
} = render();
expect(current.isValidPolicyReference).toBeTruthy();
waitForNextUpdate();
expect(getPolicyByReference).toHaveBeenCalledWith({
token,
tenant,
policyReference,
});
expect(current.isValidPolicyReference).toBeTruthy();
});
Which passes but the test for the false case does not:
test('should be falsy if the policy reference is invalid', () => {
(getPolicyByReference as jest.Mock).mockImplementationOnce(() =>
Promise.reject(new Error('policy reference error'))
);
const {
result: { current },
waitForNextUpdate,
} = render();
expect(current.isValidPolicyReference).toBeTruthy();
waitForNextUpdate();
expect(current.isValidPolicyReference).toBeFalsy();
});
It fails on two counts:
isValidPolicyReference never returns as false
There is also a console.error logged at this line setIsValid(false); where Warning: Can't perform a React state update on an unmounted component.
I am not sure how to get getPolicyByReference to throw in Jest so that the hook will return the false value.
What am I not doing correctly? Should I be wrapping my test cases in async await? Should I mock axios.get instead of getPolicyByReference?
There is also a console.error logged at this line setIsValid(false); where Warning: Can't perform a React state update on an unmounted component.
I think the issue is you are trying to test the hook alone with jest, when hooks can only be called from whithin components.
The react testing library has really nice helpers to simplify testing hooks, this article is quite informative on the matter.
I am displaying all my api requests errors in a toast.
In my code, I have separated concepts, moving the components logic to business/ui hooks.
In order to render a toast (an imperative component), I just do the following inside a functional component:
const toast = useToast(); // UI hook
toast.display(message, { type: "error", duration: 500 });
and, in order to connect to my api, I can use custom business hooks, for example:
const useRequestSomething() {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const isRequesting = useRef(false);
const requestSomething = async (someParam, onSuccess = undefined, onError = undefined) => {
if (isRequesting.current) return;
isRequesting.current = true;
setIsLoading(true);
try {
const data = await api.requestSomething(someParam);
setData(data);
onSuccess?.();
} catch(err) {
onError?.(err);
}
setIsLoading(false);
isRequesting.current = false;
}
return {
data,
isLoading,
requestSomething
}
}
My main concern is the separation of concepts... I don't think it is a good idea to use the useToast() inside the this hook that is a container of my business logic... although it may be a good idea.
So, in order to handle errors, inside any component, I can do something like:
function MyComponent() {
const toast = useToast();
const { t } = useTranslation(); // i18n.js hook
const { data, isLoading, requestSomething } = useRequestSomething();
const handleOnPress = () => {
requestSomething("x", undefined, handleOnRequestSomethingError);
}
const handleOnRequestSomethingError = (err) => {
toast.display(t(err), { type: "error", duration: 500 });
}
... JSX
}
It seems that I have defined some kind of callback-based api with the business hook... what do you think about my implementation?
Is it an anti-pattern to handle errors this way (with callbacks) inside hooks?
What is the typical approach to handle this situations? (I cannot use useQuery, because of my backend)
I think your solution is good, but, IMHO, instead of prematurely handling the error, I like to let the error propagate to where we actually know how to handle it. For example, I would do this.
const requestSomething = async (params) = {
...
try {
await api.doRequest(params);
} catch (err) {
... do some common clean up ...
throw err;
}
}
const handleOnPress = async () => {
try {
await requestSomething("x");
} catch (err) {
toast.display(t(err), { type: "error", duration: 500 });
}
}
Actually, I would wrap it in a general error handler like this.
const handleOnPress = async () => {
await withGeneralErrorHandling(async () => {
try {
await requestSomething("x");
} catch (err) {
if (err.errorCode === 'SOME_KNOWN_CASE') {
toast.display(t(err), { type: "error", duration: 500 });
} else {
throw err;
}
}
});
}
async function withGeneralErrorHandling(callback: () => Promise<void>) {
try {
await callback()
} catch (err) {
if (err.errorCode === 'GENERAL_CASE1') { ...}
else if (err.errorCode === 'GENERAL_CASE2') { ... }
else {
if (isProduction) { reportError(err); }
else { throw err; }
}
}
}
This is because I usually cannot list out all the error cases at the first implementation. Each error case will be discovered incrementally. I have to let it fail fast by letting it propagate to as closest to the outermost controller as possible.
By utilizing this built-in error propagation, you retain the stack trace information and can know exactly where the error occurs.
Yeah, your Component knows about the Toast, every future component which handles some errors will know about the Toast.
This makes your error handling logic a little rigid, if you need to use another way of handling errors in the future, you'll have to edit every component.
I'd use some state management system (redux, mobx, whatever).
The idea is that in order to show an error you need to update the state of your application. Your toast component will be subscribed to the state change and react accordingly.
This way you depend on the state, not some actual component/way of displaying errors, which is more abstract and flexible.
I want to build a React-component which loads the data asynchronously on componentDidMount.
This is what the function currently looks like (written in TypeScript):
async componentDidMount(): Promise<void> {
try {
const { props: { teamId }, state: { } } = this;
const awaitableData = await UrlHelper.getDataAsync("some-fancy-url");
// ... do something with awaitableData
} catch(e) {
console.log("Some error occured");
throw e;
}
}
The render-function returns the markup wrapped in a ErrorBoundary component, which has componentDidCatch implemented.
However, this is never called/triggered when the awaited call is rejected and I end up in the catch-block.
What am I missing here?
async function is syntactic sugar for a regular function that returns a promise. An error in async function results in rejected promise. Even if rejected promise isn't handled anywhere and results in Uncaught (in promise) error, it is not caught by error boundaries.
As the reference states,
Error boundaries do not catch errors for: <...> Asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks)
A solution is to change component state on error and handle it on next render. render is the place where error can be re-thrown synchronously.
An example:
state = { error: null };
async componentDidMount() {
try {
await new Promise(resolve => setTimeout(resolve, 2000));
throw new Error('Foo error');
} catch (error) {
this.setState({ error });
}
}
render() {
if (this.state.error) {
throw this.state.error;
}
return (
<p>Foo</p>
);
}
Using the above or below example with a functional component
provided a solution, something like this, for me:
const SomComponent: JSX.Element = () => {
const [state, setState] = useState({error: null, info: null})
... // something here setState the error
if (state.error) {
throw new Error('example') // Example throws to the error boundary
}
return <div> Some Component </div>
}
lets take a look to the Documentation
basically it says:
Error boundaries are React components that catch JavaScript errors
anywhere in their child component tree, log those errors, and display
a fallback UI instead of the component tree that crashed. Error
boundaries catch errors during rendering, in lifecycle methods, and in
constructors of the whole tree below them.
so basically when you are trying to use the ASYNC/AWAIT and it fails it will go out for the CATCH side of your function:
catch(e) {
console.log("Some error occured");
throw e;
}
and the error will not be thrown by the componentDidMount. actually if you remove the try catch method, the componentDidMount will take care of the error.
I want to build a React-component which loads the data asynchronously on componentDidMount.
This is what the function currently looks like (written in TypeScript):
async componentDidMount(): Promise<void> {
try {
const { props: { teamId }, state: { } } = this;
const awaitableData = await UrlHelper.getDataAsync("some-fancy-url");
// ... do something with awaitableData
} catch(e) {
console.log("Some error occured");
throw e;
}
}
The render-function returns the markup wrapped in a ErrorBoundary component, which has componentDidCatch implemented.
However, this is never called/triggered when the awaited call is rejected and I end up in the catch-block.
What am I missing here?
async function is syntactic sugar for a regular function that returns a promise. An error in async function results in rejected promise. Even if rejected promise isn't handled anywhere and results in Uncaught (in promise) error, it is not caught by error boundaries.
As the reference states,
Error boundaries do not catch errors for: <...> Asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks)
A solution is to change component state on error and handle it on next render. render is the place where error can be re-thrown synchronously.
An example:
state = { error: null };
async componentDidMount() {
try {
await new Promise(resolve => setTimeout(resolve, 2000));
throw new Error('Foo error');
} catch (error) {
this.setState({ error });
}
}
render() {
if (this.state.error) {
throw this.state.error;
}
return (
<p>Foo</p>
);
}
Using the above or below example with a functional component
provided a solution, something like this, for me:
const SomComponent: JSX.Element = () => {
const [state, setState] = useState({error: null, info: null})
... // something here setState the error
if (state.error) {
throw new Error('example') // Example throws to the error boundary
}
return <div> Some Component </div>
}
lets take a look to the Documentation
basically it says:
Error boundaries are React components that catch JavaScript errors
anywhere in their child component tree, log those errors, and display
a fallback UI instead of the component tree that crashed. Error
boundaries catch errors during rendering, in lifecycle methods, and in
constructors of the whole tree below them.
so basically when you are trying to use the ASYNC/AWAIT and it fails it will go out for the CATCH side of your function:
catch(e) {
console.log("Some error occured");
throw e;
}
and the error will not be thrown by the componentDidMount. actually if you remove the try catch method, the componentDidMount will take care of the error.
I have a simple react component that looks like this:
class Test extends React.Component {
componentDidMount() {
fetch('/some-url-here')
.then((data) => {
this.setState({ data });
})
.catch(() => {
alert('failed to fetch');
});
}
render() {
// render the data here
}
}
The problem with this is that the catch doesn't just catch fetch errors. It also catches any exceptions thrown in render ! What would be the correct way to create a simple component that fetches some data and handles fetch errors?
class Test extends React.Component {
componentDidMount() {
fetch('/some-url-here')
.then((data) => {
this.setState({ data });
}, (error) => {
if (error) {
// handle error here
}
});
}
render() {
// render the data here
}
}
If you use a catch instead of second callback, any errors that might've occurred during setState method would be left unhandled. So you can capture render method errors in your own way.
For more info, give a reading on this tweet by Dan Abramov.
Dan's Tweet