Introduction
I am handling all the errors in my app. I have noticed there are different types of errors that can be handled in React...
Errors with rendering (undefined props, ...), which can be handled with an Error Boundary Component, with custom Fallback Components.
Errors coming from the backend, which I want to display in a toast with i18n.js for translating the messages.
Is it common to combine these two types of error handling in React apps?
Displaying errors in a toast
I have decided to create my own React Context
which renders toasts for notifications, errors, etc. Here is my implementation:
/* eslint-disable react/prop-types */
import React, {
createContext,
useReducer,
useMemo,
} from "react";
import toastReducer, {
initialState,
actionCreators,
} from "./helpers/reducers/toastReducer";
import { TOAST_TYPES, DEFAULT_TOAST_DURATION } from "../../utils/toast";
import Toast from "../../components/Toast";
const ToastContext = createContext(null);
export default ToastContext;
export function ToastProvider({ children }) {
const [toast, dispatch] = useReducer(toastReducer, initialState);
const open = (
type = TOAST_TYPES[0],
message,
duration = DEFAULT_TOAST_DURATION,
action = undefined
) => {
dispatch(actionCreators.open(type, message, duration, action));
};
const close = () => {
dispatch(actionCreators.close());
};
const value = useMemo(() => ({
open,
close,
}), []);
return (
<ToastContext.Provider value={value}>
{children}
<Toast
visible={toast.visible}
type={toast.type}
duration={toast.duration}
action={toast.action}
onDismiss={close}
>
{toast.message}
</Toast>
</ToastContext.Provider>
);
}
I have also implemented my useToast() hook that consumes this context.
Fetching data from server
As I am using hooks, I am saving the errors in a state "error" on each custom hook that fetches data from my DB.
// Based on the library "useQuery"
const useFetchSomeData = ({ fetchOnMount = true } = {}) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const cursor = useRef(new Date());
const isFetching = useRef(false);
const fetchData = async () => {
if (isFetching.current) return;
isFetching.current = true;
setLoading(true);
try {
const data = await api.fetchData(cursor.current);
} catch(err) {
setError(err);
} finally {
setLoading(false);
isFetching.current = false;
}
}
useEffect(() => {
if (fetchOnMount) {
fetchData();
}
}, []);
return {
data,
loading,
error,
fetchData
}
}
And in my components I just do:
const { data, error, loading } = useFetchData();
const toast = useToast();
const { t } = useI18N();
useEffect(() => {
if (error) {
toast.open("error", t(err.code));
}
}, [error, t]);
Questions
As you can see, in my components, I am just using a
useEffect
for sending the translated errors to my toast context. But... what if I get the same error response from the server twice? I mean, the error is not reset tonull
in the custom hook... how can I do that?Is this implementation robust (in your opinion) and typical in these situations?
CodePudding user response:
Honestly I'm a bit confused, I think I understand what you are trying to do but it doesn't have to be this complicated.
You should wrap your entire app with a ToastContainer
or ToastContext
(whatever you want to call it) and import your toast whenever needed then call Toat.show("Error: ...")
. Checkout the library react-toastify
for more details, maybe this library could help you. You wrap your project at the app entry level and never have to worry about it again.
As far as error handling, when creating a function to fetch data you should expect one error at a time and handle it appropriately. I personally think it's best to return the error to the original component that called the fetch function and handle it there. It's easier to keep track of things this way in my opinion, but what you're doing is not wrong.
Error handling takes time and thinking ahead, I'm not gonna deny it, but it makes a big different between a badly constructed app and a well thought out app.
I'm also not completely sure why you are using const isFetching = useRef(false);
when you could just use const [isFetching, setIsFecthing] = useState(false)
, this is what useState
is for. Same for useMemo
. I think things could be leaner here, as you app grows and becomes more complex it'd easier to maintain.