I am using redux toolkit query for my data fetching API.
The problem is I am having is error handling redux toolkit returns an error property from the query hook. So the error that is returned from the server is an object with nested data and what trying to access it I get a typescript error when trying to access the data from the error object.
below is how I am declaring my mutation hook
const [updateProgram, {
isLoading: isUpdatingProgram,
error: updateProgramError
}] = useUpdateProgramMutation();
below is how I try to access the error in my code.
onSubmit = {
async(values, {
setSubmitting
}) => {
await updateProgram({ ...values,
id: 'programData.data._id'
});
refetch();
if (updateProgramError) {
enqueueSnackbar('Test message', {
variant: 'error'
});
console.log(updateProgramError?.data?.message);
}
setSubmitting(false);
}
}
now the typescript error I am getting is as below.
Not that I able to console.log(updateProgramError) and is the data in the console
Property 'data' does not exist on type 'FetchBaseQueryError | SerializedError'.
Property 'data' does not exist on type 'SerializedError'.
below is updateProgramError when I log is as a whole on the console
{
"status": 500,
"data": {
"status": "error",
"message": "Something went very wrong!"
}
}
below is how I have implemented useUpdateProgramMutation().
import { ndovuAPI } from './ndovuAPI';
export const ndovuProgramsAPI = ndovuAPI.injectEndpoints({
endpoints: builder => ({
getProgramById: builder.query({
query: (id: string) => `/programs/${id}`,
providesTags: ['Programs']
}),
getAllPrograms: builder.query({
query: queryParams => ({ url: `/programs/`, params: queryParams }),
providesTags: ['Programs']
}),
registerProgram: builder.mutation({
query: body => ({
url: '/programs',
method: 'POST',
body
}),
invalidatesTags: ['Programs']
}),
updateProgram: builder.mutation({
query: body => ({ url: `/programs/${body.id}`, method: 'PATCH', body }),
invalidatesTags: ['Programs']
})
}),
overrideExisting: true
});
// Export hooks for usage in functional components
export const { useGetProgramByIdQuery, useGetAllProgramsQuery, useUpdateProgramMutation } = ndovuProgramsAPI;
The code as you have it written will not work the way you want. You can't reference the error from the hook inside of an onSubmit handler like that. What you'll want to do is this:
const onSubmit = async (values) => {
try {
// unwrapping will cause data to resolve, or an error to be thrown, and will narrow the types
await updateProgram({
...values,
id: 'programData.data._id'
}).unwrap();
// refetch(); // you should most likely just use tag invalidation here instead of calling refetch
} catch (error) {
// error is going to be `unknown` so you'll want to use a typeguard like:
if (isMyKnownError(error)) { // add a typeguard like this
enqueueSnackbar('Test message', {
variant: 'error'
});
} else {
// You have some other error, handle it differently?
}
}
}
References:
https://redux-toolkit.js.org/rtk-query/usage/mutations#frequently-used-mutation-hook-return-values
From what I can infer from this TS error, the type of updateProgramError seems like an union of types 'FetchBaseQueryError', 'SerializedError' and possibly other types, which means that TS only assumes that you can safely access this data property after ensuring that data exists in the object.
In other words, TS does not know which of these types updateProgramError will be unless you do some checking to ensure that.
if (updateProgramError) {
enqueueSnackbar('Test message', {
variant: 'error'
});
if ('data' in updateProgramError) console.log(updateProgramError.data.message);
}
Related
I've followed the docs (and some other posts) about RTK optimistic updates but I can't seem to get my case to work. Here is my createReply mutation:
createReply: builder.mutation<IPost, any>({
query({...body}) {
return {
url: PostPath,
method: 'POST',
body: body,
};
},
async onQueryStarted({...body}, {dispatch, queryFulfilled}) {
const patchResult = dispatch(
resourcesApi.util.updateQueryData('getRepliesById', body.parent, (draft) => {
console.log(draft);
Object.assign(draft, body)
})
)
try {
await queryFulfilled
} catch {
console.log("query was fulfilled");
console.log(patchResult);
// patchResult.undo()
}
}
}),
and this is the getRepliesById query that it's referencing:
getRepliesById: builder.query<IPost, string>({
query: (id: string) => `${PostPath}/${id}/replies`,
}),
When I try to use this mutation, the POST works fine, but I'm given this error:
[Immer] Immer only supports setting array indices and the 'length' property.
(The error call stack ends with onQueryStarted).
Where here am I going against Immer rules?
Edit; Here is body:
const postObj = {
content: reply,
author: user._id,
parent: _parentId,
visibility: {scope: 'public'},
media: [],
ats: [],
kind: 'comment',
};
createReply(postObj);
It seems that data is an array and now you are working with it as if it were an object. An array can only have number-indexed properties and your Object.assign seems to add other properties than that?
I am new to react query and tried many tutorials still I am not logging anything and nothing happens after the following code, cannot even log new data, It doesnt feel like there is any error. POST is working perfectly.
addCategory: builder.mutation({
query: (body) => ({
method: "POST",
url: apiLinks.categoryPUT,
body: body,
}),
async onQueryStarted(body, { dispatch, queryFulfilled }) {
try {
const res = await queryFulfilled;
const id = _.get(res, "data.data.InsertedID", Date.now());
dispatch(
categoryApi.util.updateQueryData(
"getAllCategory",
undefined,
(data) => {
const newBody = { id, ...body };
console.log({ data, newBody }); //nothing shows here
data.push(newBody);
}
)
);
} catch (error) {
console.log({ error }); // nothing logs here
}
},
}),
problem was I used this: useGetAllCategoryQuery("getAllCat"); to fetch data.
useGetAllCategoryQuery("getAllCat");
so it should be:
categoryApi.util.updateQueryData(
"getAllCategory",
"getAllCat", //change here
seems its a unique identifier for cache data
I have some requests which may return 404s. When they do, RTK query will send retries, resulting in hundreds of failed requests. Why is it trying to refetch on error and what can I do?
If your endpoint is in error, RTK Query's useQuery will send a request in two situations:
you change the argument (that would always result in a new request)
you mount a component using this useQuery.
So without seeing your code, I would assume that your component re-mounts somehow and thus leads to another request after mounting.
you can limit the number of retries that rtk automatically does by using the property maxRetries inside your end point.
import { createApi, fetchBaseQuery, retry } from
'#reduxjs/toolkit/query/react'
// maxRetries: 5 is the default, and can be omitted. Shown for
documentation purposes.
const staggeredBaseQuery = retry(fetchBaseQuery({ baseUrl: '/' }), {
maxRetries: 5,
})
export const api = createApi({
baseQuery: staggeredBaseQuery,
endpoints: (build) => ({
getPosts: build.query({
query: () => ({ url: 'posts' }),
}),
getPost: build.query({
query: (id) => ({ url: `post/${id}` }),
extraOptions: { maxRetries: 5 }, // You can override the retry behavior on each endpoint
}),
}),
})
export const { useGetPostsQuery, useGetPostQuery } = api
As docs say, for custom error handling we can use queryFn:
One-off queries that use different error handling behaviour
So if, for any reason, you want to cache request on error, you can do:
getPokemon: build.query<Pokemon, string>({
async queryFn(name, api, extraOptions, baseQuery) {
const result = await baseQuery({
url: `https://pokeapi.co/api/v2/pokemon/${name}`,
method: 'GET'
});
if (result.error?.status === 404) {
// don't refetch on 404
return { data: result.data as Pokemon };
}
if (result.error) {
// but refetch on another error
return { error: result.error };
}
return { data: result.data as Pokemon };
}
}),
You need to customize your createApi function. you can stop permanently retries with setting unstable__sideEffectsInRender property to false
import {
buildCreateApi,
coreModule,
reactHooksModule,
} from '#reduxjs/toolkit/dist/query/react';
const createApi = buildCreateApi(
coreModule(),
reactHooksModule({ unstable__sideEffectsInRender: false })
);
export default createApi;
I found an online example of a custom api service hook with typescript that seemed to make sense to me so I tried applying it to my needs. There are a couple things that are happening that don't make sense to me. First off, when I log out the data coming back from the api, it's being logged twice which I don't understand.
Secondly, when I log out service.payload.results, 'results' is undefined, but if I log just service.payload it logs an array as expected (except twice). I'm trying to type the results of my api call and enforce the shape on the data. I can't figure out what it is. Here is my code. Please see if you can spot anything that I'm missing or clearly do not understand.
Domain Types (Visitor.ts):
export interface Visit {
id: string;
visited: string;
}
export interface Visitor {
id: string;
name: string;
visits: Array<Visit>;
}
Service Types (Service.ts):
interface ServiceInit {
status: "init";
}
interface ServiceLoading {
status: "loading";
}
interface ServiceLoaded<T> {
status: "loaded";
payload: T;
}
interface ServiceError {
status: "error";
error: Error;
}
export type Service<T> =
| ServiceInit
| ServiceLoading
| ServiceLoaded<T>
| ServiceError;
Custom Hook:
import { useEffect, useState } from "react";
import Axios from "axios";
import { Service } from "../types/Service";
import { Visitor } from "../types/Visitor";
export interface Visitors {
results: Visitor[];
}
const useVisitorService = () => {
const [result, setResult] = useState<Service<Visitors>>({
status: "loading",
});
const url: string = "api/visitors";
const config = {
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
};
useEffect(() => {
const requestData = async function () {
try {
const res = await Axios.get(url, config);
setResult({ status: "loaded", payload: res.data });
}
catch (error) {
setResult({ status: "error", error });
}
};
requestData();
}, []);
return result;
};
export default useVisitorService;
Component that is using the hook (VisitList.tsx) :
import React from "react";
import useVisitorService from "../services/useVisitorService";
const VisitList: React.FC<{}> = () => {
const service = useVisitorService();
if (service.status === "loaded") {
// this logs the expected array of objects from server (twice)
console.log(service.payload);
}
//this errors because results is undefined
return (
<div>
{service.status === "loading" && <div>Loading...</div>}
{service.status === "loaded" &&
service.payload.results.map((visitor) => (
<div key={visitor.id}>{visitor.name}</div>
))}
{service.status === "error" && (
<div>Error.</div>
)}
</div>
);
};
export default VisitList;
This is a tricky one and I'm not entirely sure what's going on. Can you set up a sandbox which has the full url to fetch?
The potential problem that I see may or may not be an issue, depending on what the returned res object looks like. I suspect that you are setting service.payload instead of service.payload.results, but I could be wrong.
Through your types, you have said that a successful result looks like this:
interface ServiceLoaded {
status: "loaded";
payload: {
results: Visitor[];
}
}
where payload must have a property results which is an array of Visitor objects.
This is not explicitly stated or verified in your code, which leaves it prone to errors. You are setting the payload to res.data which could be any type.
setResult({ status: "loaded", payload: res.data });
Does res.data contain a property results that lists the visitors? If it merely returns a list of visitors, you need to assign it to the results property yourself, and this would be the source of your troubles. (Or you need to change your type/interface Visitors so that is just an array Visitor[] and not an object containing the array.)
setResult({ status: "loaded", payload: results: { res.data } });
Without knowing what your res.data actually looks like, I can suggest that you use a typescript type guard to verify it, since the Axios res.data currently has type any. That way you know that payload.results will always be defined if service.status === "loaded". If Axios returns an unexpected result, setResult should store an error.
Your type guard is something like this. I am just checking that property results exists, but you could get a lot more detailed and check that it is an array and that every element is a Vistor.
const isValidPayload = (payload: any): payload is Visitors => {
return typeof payload === "object" && "results" in payload;
}
Your requestData function now looks something like, where we are still catching errors returned from Axios, but also catching errors due to invalid responses.
const requestData = async function () {
try {
const res = await Axios.get(url, config);
if ( isValidPayload(res.data) ) {
setResult({ status: "loaded", payload: res.data});
} else {
setResult({ status: "error", error: {
name: "Invalid Format",
message: "API results must contain an array of Visitor objects",
}});
}
}
catch (error) {
setResult({ status: "error", error });
}
};
I want to show some errors that comes from graphql server to user.
Have some component with callback that use some mutation
onSave() => {
this.props.mutate({
mutation: CHANGE_ORDER_MUTATION,
variables: {
ids,
},
}).catch((e) => {
console.log(e.graphQLErrors) <--- e.graphQLErrors is always empty array = []
})
}
While I'm able to see the graphQLErrors error with apollo-link-error link.
const errorLink = onError(({ graphQLErrors, networkError }) => {
console.log(graphQLErrors) <--- errors from server
});
Migration to apollo-server instead of express-graphql solve the problem.
Or you can access errors by e.networkError.result.errors
As I understand it, the 'catch' will catch errors if your server returns an error code, but not if it returns your errors in the response, but with a success code. In that case, you just need to get your errors out of the response (keeping your catch too in case the server responds with an error code):
this.props.mutate({
mutation: CHANGE_ORDER_MUTATION,
variables: {
ids,
},
})
.then((response) => {
if (response.errors) {
// do something with the errors
} else {
// there wasn't an error
}
})
.catch((e) => {
console.log(e.graphQLErrors) <--- e.graphQLErrors is always empty array = []
})
Alternatively - if you use the graphql() option from react-apollo, you can specify an onError handler:
export default graphql(CHANGE_ORDER_MUTATION, {
options: (props) => ({
onCompleted: props.onCompleted,
onError: props.onError
}),
})(MyComponent);
references:
https://github.com/apollographql/react-apollo/issues/1828
Apollo client mutation error handling