T
TanStack15mo ago
manual-pink

Alter error object returned from queries

Hi there! I'm trying to implement localized errors, by throwing errors from the server containing translation keys. Currently I just have a custom hook, like useErrorMessage (see attached image) for handling this. I'm wondering if it is possible to configure the QueryClient in a way so that all my queries are automatically returning a new property like, I18nErrorMessage, that is the result of running my custom hook? Or do I have to go with the other more manual solution which would probably be to create a custom hook for each query/mutation like useBooksQuery and inside that custom hook I use my useErrorMessage hook and return the translated message together with the query itself?
No description
22 Replies
grumpy-cyan
grumpy-cyan15mo ago
I think you are making things hard for yourself. IMO translations are part of the view. In the view you are also already using useTranslation, so you can just call t(error…..) there. No need for a custom hook I guess?
manual-pink
manual-pinkOP15mo ago
I'm for sure making things harder for myself, but I'm just trying to see if I can make an abstraction that will eliminate the need for me to call the useTranslations hook manually every time - and even eliminate the need to create a customHook (if I can just add on a property to the error object. I just want to see if it is possible) I was wondering if you could just add on an extra property to the error object in the onError function in the QueryCache and MutationCache instances - or if this is gonna be harmful. Since these caches and the QueryClient are created outside of react, I would need to create my own translator function (and not use the useTranslation hook), luckily I can do that with the use-intl package. Here is a potential implementation: QueryClient:
const [queryClient] = useState(
() =>
new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
handleGlobalQueryClientErrors(error, query);
addTranslatedErrorMessage(error) <-- adding the translated error message here (see implementation below)
},
}),
mutationCache: new MutationCache({
onError: (error) => {
handleGlobalMutationClientErrors(error);
addTranslatedErrorMessage(error) <-- adding the translated error message here (see implementation below)
},
}),
}),
);
const [queryClient] = useState(
() =>
new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
handleGlobalQueryClientErrors(error, query);
addTranslatedErrorMessage(error) <-- adding the translated error message here (see implementation below)
},
}),
mutationCache: new MutationCache({
onError: (error) => {
handleGlobalMutationClientErrors(error);
addTranslatedErrorMessage(error) <-- adding the translated error message here (see implementation below)
},
}),
}),
);
adding the translated error message onto the error object
const translatorCache = new Map<
SupportedLanguagesEnum,
ReturnType<typeof createTranslator>
>();

const getTranslator = () => {
// Check if the translator for this locale is already cached
const locale = settings$.state.locale.get() ?? CONSTANTS.DEFAULT_LOCALE; // observer
let translator = translatorCache.get(locale);

// If not cached, create a new translator and cache it

if (!translator) {
const messages = LanguageMessages[locale];
translator = createTranslator({
locale: locale,
// TODO: fix type
messages: messages as any,
});
translatorCache.set(locale, translator);
}

return translator;
};

export const addTranslatedErrorMessage = (error: Error) => {
const t = getTranslator();

if (ClientErrorUtils.isTRPCClientError(error)) {
if (error.data?.translationKey && error.data.translationKey in messages) {
error.translatedMessage = t(error.data.translationKey);
}
} else {
error.translatedMessage = t('errors.generic.something_went_wrong');
}
};
const translatorCache = new Map<
SupportedLanguagesEnum,
ReturnType<typeof createTranslator>
>();

const getTranslator = () => {
// Check if the translator for this locale is already cached
const locale = settings$.state.locale.get() ?? CONSTANTS.DEFAULT_LOCALE; // observer
let translator = translatorCache.get(locale);

// If not cached, create a new translator and cache it

if (!translator) {
const messages = LanguageMessages[locale];
translator = createTranslator({
locale: locale,
// TODO: fix type
messages: messages as any,
});
translatorCache.set(locale, translator);
}

return translator;
};

export const addTranslatedErrorMessage = (error: Error) => {
const t = getTranslator();

if (ClientErrorUtils.isTRPCClientError(error)) {
if (error.data?.translationKey && error.data.translationKey in messages) {
error.translatedMessage = t(error.data.translationKey);
}
} else {
error.translatedMessage = t('errors.generic.something_went_wrong');
}
};
grumpy-cyan
grumpy-cyan15mo ago
Are you not using useTranslate where you display the error?
manual-pink
manual-pinkOP15mo ago
I could do that, but I'm trying to implement an abstraction where the error object from the useQuery hook already has the translated message. I'm just trying to figure out if this is possible, doing more harm than good, etc etc Pretty sure I need to have this translation function (that is not a hook) anyways, If I want to localize global errors - without having to recreate the QueryClient. I asked a question related to the above here Why I wanted to create a custom hook, was to have a fallback to a generic key just in case a translation key is not sent or a non-existent key is sent from the server
grumpy-cyan
grumpy-cyan15mo ago
How does it solve the non-existent key problem?
helpful-purple
helpful-purple15mo ago
@kgni I would suggest you using meta object for this and create a global error handling. It will take a little bit more effort to setup everything, but I guess that's your best option. https://tkdodo.eu/blog/react-query-error-handling https://tkdodo.eu/blog/automatic-query-invalidation-after-mutations#use-the-meta-option
React Query Error Handling
After covering the sunshine cases of data fetching, it's time to look at situations where things don't go as planned and "Something went wrong..."
Automatic Query Invalidation after Mutations
Even though there is nothing built into React Query, it doesn't need a lot of code to implement automatic query invalidation in user-land thanks to the global cache callbacks.
manual-pink
manual-pinkOP15mo ago
I refactored the hook so that Im checking if the key exists in my en.json file, I can send an example when I'm back home if needed Cool thanks, will have a look when I get back home!
helpful-purple
helpful-purple15mo ago
Just open a link not a thumbnail, because thumbnail doesn't include an anchor for some reason
grumpy-cyan
grumpy-cyan15mo ago
But the meta option is static 😉
helpful-purple
helpful-purple15mo ago
So what? You can still provide a key for the translate function or am I missing something?
grumpy-cyan
grumpy-cyan15mo ago
The key is dynamic as it comes from the queryFn
helpful-purple
helpful-purple15mo ago
Oh, I missed that. Then we need to adjust a little bit. For example, I've created a custom class that wraps axios instance and transforms response and error. This way I'm not leaking axios instances to the application layer and have more control over the error and response object. I'm using Axios interceptors to catch errors and provide my own error in the application. In my case, I simply transformed the error object to be able to access its properties for validation purposes easily.
manual-pink
manual-pinkOP15mo ago
Ah of course! I think I had the wrong approach to begin with, the query client should probablt not be responsible for modifying the error object...
Instead this should be done in an interceptor -> and in that interceptor I can simply just run the translation key through my translation function. The only "downside" or the only thing I'm unsure about is that translation function in the interceptor. Because now I'm both using a useTranslation hook when in react, but also using the other function in the interceptor. Not sure if it is bad to split it up like this, instead of just having the hook being responsible for it like it is in the very first example I provided in the post.
helpful-purple
helpful-purple15mo ago
There's actually no good or bad solution. I would agree that providing translation is probably the responsibility of the view layer, but depending on your use case having a single place for handling this logic may be appropriate.
manual-pink
manual-pinkOP15mo ago
yeah, @M00LTi also mentioned this as the very first thing to just make the view responsible. Well I learned quite some things exploring all of this - thanks a lot for your time both of you! I just have one final "issue" related to global errors and localization. Since this is handled inside of the queryCache and mutationCache, I can't use the useTranslation hook, so I guess in that case I actually need to create a separate translation function like this
helpful-purple
helpful-purple15mo ago
Yeah, queryClient is a class, it lives outside the react world, so you can not use hooks in it
manual-pink
manual-pinkOP15mo ago
I think my implementation for that probably fine, as I'm still connecting it to the already existing message json files, ensuring that if I add other langauge this will already be handled.
helpful-purple
helpful-purple15mo ago
I can not tell if it's ok without testing it, but I think you will do just fine.
manual-pink
manual-pinkOP15mo ago
I have a function like this that I can use inside of the queryCache and queryMutation onError. I'm getting the current locale through an observer. Seems to be working when I'm testing it out
const translatorCache = new Map<
SupportedLanguagesEnum,
ReturnType<typeof createTranslator>
>();

const getTranslator = () => {
// Check if the translator for this locale is already cached
const locale = settings$.state.locale.get() ?? CONSTANTS.DEFAULT_LOCALE; // observer
let translator = translatorCache.get(locale);

// If not cached, create a new translator and cache it

if (!translator) {
const messages = LanguageMessages[locale];
translator = createTranslator({
locale: locale,
// TODO: fix type
messages: messages as any,
});
translatorCache.set(locale, translator);
}

return translator;
};
const translatorCache = new Map<
SupportedLanguagesEnum,
ReturnType<typeof createTranslator>
>();

const getTranslator = () => {
// Check if the translator for this locale is already cached
const locale = settings$.state.locale.get() ?? CONSTANTS.DEFAULT_LOCALE; // observer
let translator = translatorCache.get(locale);

// If not cached, create a new translator and cache it

if (!translator) {
const messages = LanguageMessages[locale];
translator = createTranslator({
locale: locale,
// TODO: fix type
messages: messages as any,
});
translatorCache.set(locale, translator);
}

return translator;
};
and then I have a handler like this:
const handleGlobalQueryClientErrors = (
error: Error,
query: Query<unknown, unknown, unknown, QueryKey>,
) => {
// only show error toasts if we already have data in the cache, which indicates a failed background update
// or if we get an internal server error.
// Rest of the errors will be handled locally in the app (specific error messages)

const t = getTranslator();

if (
query.state.data !== undefined ||
(error instanceof TRPCClientError && error.data.httpStatus >= 500)
) {
ToastService.showError({
text1: t('errors.generic.something_went_wrong'),
});
} else if (
isAxiosError(error) &&
error.response?.status &&
error.response.status >= 500
) {
ToastService.showError({
text1: t('errors.generic.something_went_wrong'),
});
}
};
const handleGlobalQueryClientErrors = (
error: Error,
query: Query<unknown, unknown, unknown, QueryKey>,
) => {
// only show error toasts if we already have data in the cache, which indicates a failed background update
// or if we get an internal server error.
// Rest of the errors will be handled locally in the app (specific error messages)

const t = getTranslator();

if (
query.state.data !== undefined ||
(error instanceof TRPCClientError && error.data.httpStatus >= 500)
) {
ToastService.showError({
text1: t('errors.generic.something_went_wrong'),
});
} else if (
isAxiosError(error) &&
error.response?.status &&
error.response.status >= 500
) {
ToastService.showError({
text1: t('errors.generic.something_went_wrong'),
});
}
};
helpful-purple
helpful-purple15mo ago
Looks ok at a first glance
manual-pink
manual-pinkOP15mo ago
once again, thanks for your time! really appreciate it
helpful-purple
helpful-purple15mo ago
you're welcome 🙂

Did you find this page helpful?