React-query causes the whole React tree to unmount on response error - reactjs

As it is said here React docs, on uncaught errors the whole tree will unmount.
Query 1:
const { data: post } = useQuery<PostResponseDto, AxiosError>(
['fetch-post', params],
() => fetchPost(params!.postId),
{
refetchOnMount: true,
onError: (err) => {
// do something with the error
},
},
);
Query 2:
const {
data: postCommentsGroups,
fetchNextPage,
isFetchingNextPage,
hasNextPage,
refetch,
} = useInfiniteQuery<PostCommentsResponseDto, AxiosError>(
['fetch-post-comments', params],
({ pageParam = 0 }) => fetchPostComments(params!.postId, pageParam),
{
refetchOnMount: true,
getNextPageParam: (lastPage) => {
if (!lastPage.data.nextPage) {
return false;
} else {
return lastPage.data.nextPage;
}
},
onError: (err) => {
// do something with the error
},
},
);
Fetch functions:
export const fetchPost = async (webId: string) => {
const response = await axiosInstance.get(`${API_URL}/post/${webId}`);
return response.data;
};
export const fetchPostComments = async (webId: string, page: number) => {
const response = await axiosInstance.get(
`${API_URL}/post/${webId}/comments?page=${page}&limit=${COMMENTS_LIMIT}`,
);
return response.data;
};
The problem: When these queries result in status code !== 200, uncaught errors appear in the console (see below) and the whole React tree unmounts. This definitely is not the behavior I want. E.g. on both of these queries I get 404 if postId is incorrect and when this happens I want to do certain actions in onError callback (show some info to the user), but this impossible due to the uncaught errors by react-query and React unmounting the whole tree.
This is one of the few uncaught errors (AxiosErrors).
Why is this happening?
P.S.: I don't think this happens with useMutate() hook.
P.P.S: I am using global ErrorBoundary but that is irrelevant for this problem. I want to manipulate the DOM in that specific component in which queries are being made/errored.

Related

Next js: Hydration failed with React Query and React Cookie

So i have narrowed the issue down to the fact that I am using the cookie.logged_in to enable the react query call. If I remove the enabled: !!cookies.logged_in and cookies.logged_in in the if condition, then the code works properly
. I want a scenario where the code goes directly to show the children if the cookies.logged_in is unavailable and only tries to show the loading when both the query. loading is working and cookies.logged_in is actually available
This is my code
const AuthMiddleware: React.FC<AuthMiddlewareProps> = ({ children }) => {
const [cookies] = useCookies(['logged_in']);
const { setCurrentUser } = useContext(Context);
const query = useQuery(['authUser'], () => getCurrentUserFn(), {
enabled: !!cookies.logged_in,
select: (data) => data,
onSuccess: (response) => {
setCurrentUser(response);
},
onError: () => {
setCurrentUser({} as AuthUser);
},
});
if (query.isLoading && cookies.logged_in) {
return <LoadingScreen />;
}
return children;
};

Multiple useLazyQuery hooks (Apollo Client) in React Function Component

I am trying to include two Apollo-Client useLazyQuery hooks in my function component. Either works fine alone with the other one commented out, but as soon as I include both, the second one does nothing. Any ideas?
export default function MainScreen(props) {
useEffect(() => {
validateWhenMounting();
}, []);
const [validateWhenMounting, { loading, error, data }] = useLazyQuery(
validateSessionToken,
{
onCompleted: (data) => console.log('data', data),
},
);
const [validate, { loading: loading2, error: error2, data: data2 }] =
useLazyQuery(validateSessionTokenWhenSending, {
onCompleted: (data2) => console.log('data2', data2),
});
const handleSendFirstMessage = (selectedCategory, title, messageText) => {
console.log(selectedCategory, title, messageText);
validate();
};
Figured it out: Adding the key-value pair fetchPolicy: 'network-only', after onCompleted does the trick. It seems that otherwise, no query is being conducted due to caching...
This is the pattern that I was talking about and mentioned in the comments:
const dummyComponent = () => {
const [lazyQuery] = useLazyQuery(DUMMY_QUERY, {variables: dummyVariable,
onCompleted: data => // -> some code here, you can also accept an state dispatch function here for manipulating some state outside
onError: error => // -> you can accept state dispatch function here to manipulate state from outside
});
return null;
}
this is also a pattern that you are going to need sometimes

Undefined data (sometimes data)

I'm trying to read an Array inside my FireStore document. I want to render the items inside this Array in a component through using .map().
Sometimes, I get a TypeError: Cannot read property 'map' of undefined error. What could be causing it and how can I ensure that it doesn't happen.
interface Product {
summary: string;
details: string;
product: string;
benefit: Array<string>;
}
function ProductInfo({ product }: { product: Product }) {
console.log("Product:",product.summary);
product.benefit.forEach((item) => { //triggers exception sometimes
console.log(item)
})
}
In a different component, this is how I populate the data and pass it to the component above:
function ProductDetails({ match }: RouteComponentProps<TParams>) {
const [product, setProduct]: any = useState([]);
useEffect(() => {
const fetchData = async () => {
try {
const response = await db.collection("Products").doc(match.params.id).get();
console.log('response', response.data());
let data: any = { title: 'not found' };
if (response.exists) {
data = response.data();
}
setProduct(data);
} catch (err) {
console.error(err);
}
};
fetchData();
}, []);
return (
<div>
<ProductInfo product={product} />
</div>
)
}
I'm learning React with TypeScript so I'm still trying to get the hang of things. I'm confused as to why it works sometimes and does not at other instances. product.summary gets rendered all the time though. Thank you
EDIT
From debugging, I think I seem to have found the issue:
If I test with:
const [product, setProduct]: any = useState();
useEffect(() => {
const fetchData = async () => {
try {
const response = await db.collection("Products").doc("flex-crm").get();
console.log('response', response.data());
let data: any = { title: 'not found' };
if (response.exists) {
data = response.data();
}
setProduct(data);
} catch (err) {
console.error(err);
}
};
fetchData();
}, []);
console.log("Data: ", product)
I get 3 lines of output in the console (instead of 2). I get
Data: undefined
response {...}
Data: {...}
My prop is using the Data:undefined instance when the component is rendered. How can I update it to use the fetched data?
Try moving setProduct(data); inside the if condition. Maybe for some products response is not present and you are still setting data which will be undefined in that case.
if (response.exists) {
data = response.data();
setProduct(data);
}
Try to use product?.summary.map(...).
Probably this will help you.
~Also why did you use response.data() i could not get it, shouldn't it be response.data?

useEffect infinite loop occurs only while testing, not otherwise - despite using useReducer

I'm trying to test a useFetch custom hook. This is the hook:
import React from 'react';
function fetchReducer(state, action) {
if (action.type === `fetch`) {
return {
...state,
loading: true,
};
} else if (action.type === `success`) {
return {
data: action.data,
error: null,
loading: false,
};
} else if (action.type === `error`) {
return {
...state,
error: action.error,
loading: false,
};
} else {
throw new Error(
`Hello! This function doesn't support the action you're trying to do.`
);
}
}
export default function useFetch(url, options) {
const [state, dispatch] = React.useReducer(fetchReducer, {
data: null,
error: null,
loading: true,
});
React.useEffect(() => {
dispatch({ type: 'fetch' });
fetch(url, options)
.then((response) => response.json())
.then((data) => dispatch({ type: 'success', data }))
.catch((error) => {
dispatch({ type: 'error', error });
});
}, [url, options]);
return {
loading: state.loading,
data: state.data,
error: state.error,
};
}
This is the test
import useFetch from "./useFetch";
import { renderHook } from "#testing-library/react-hooks";
import { server, rest } from "../mocks/server";
function getAPIbegin() {
return renderHook(() =>
useFetch(
"http://fe-interview-api-dev.ap-southeast-2.elasticbeanstalk.com/api/begin",
{ method: "GET" },
1
)
);
}
test("fetch should return the right data", async () => {
const { result, waitForNextUpdate } = getAPIbegin();
expect(result.current.loading).toBe(true);
await waitForNextUpdate();
expect(result.current.loading).toBe(false);
const response = result.current.data.question;
expect(response.answers[2]).toBe("i think so");
});
// Overwrite mock with failure case
test("shows server error if the request fails", async () => {
server.use(
rest.get(
"http://fe-interview-api-dev.ap-southeast-2.elasticbeanstalk.com/api/begin",
async (req, res, ctx) => {
return res(ctx.status(500));
}
)
);
const { result, waitForNextUpdate } = getAPIbegin();
expect(result.current.loading).toBe(true);
expect(result.current.error).toBe(null);
expect(result.current.data).toBe(null);
await waitForNextUpdate();
console.log(result.current);
expect(result.current.loading).toBe(false);
expect(result.current.error).not.toBe(null);
expect(result.current.data).toBe(null);
});
I keep getting an error only when running the test:
"Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render."
The error is coming from TestHook: node_modules/#testing-library/react-hooks/lib/index.js:21:23)
at Suspense
I can't figure out how to fix this. URL and options have to be in the dependency array, and running the useEffect doesn't change them, so I don't get why it's causing this loop. When I took them out of the array, the test worked, but I need the effect to run again when those things change.
Any ideas?
Try this.
function getAPIbegin(url, options) {
return renderHook(() =>
useFetch(url, options)
);
}
test("fetch should return the right data", async () => {
const url = "http://fe-interview-api-dev.ap-southeast-2.elasticbeanstalk.com/api/begin";
const options = { method: "GET" };
const { result, waitForNextUpdate } = getAPIbegin(url, options);
expect(result.current.loading).toBe(true);
await waitForNextUpdate();
expect(result.current.loading).toBe(false);
const response = result.current.data.question;
expect(response.answers[2]).toBe("i think so");
});
I haven't used react-hooks-testing-library, but my guess is that whenever React is rendered, the callback send to RenderHook will be called repeatedly, causing different options to be passed in each time.

useLazyQuery causing too many re-renders [Apollo/ apollo/react hooks]

I'm building a discord/slack clone. I have Channels, Messages and users.
As soon as my Chat component loads, channels get fetched with useQuery hook from Apollo.
By default when a users comes at the Chat component, he needs to click on a specific channels to see the info about the channel and also the messages.
In the smaller Channel.js component I write the channelid of the clicked Channel to the apollo-cache. This works perfect, I use the useQuery hooks #client in the Messages.js component to fetch the channelid from the cache and it's working perfect.
The problem shows up when I use the useLazyQuery hook for fetching the messages for a specific channel (the channel the user clicks on).
It causes a infinite re-render loop in React causing the app to crash.
I've tried working with the normal useQuery hook with the skip option. I then call the refetch() function when I need it. This 'works' in the sense of it not giving me infinite loop.
But then the console.log() give me this error: [GraphQL error]: Message: Variable "$channelid" of required type "String!" was not provided. Path: undefined. This is very weird because my schema and variables are correct ??
The useLazyQuery does give me infinite loop as said before.
I'm really struggling with the conditionality of apollo/react hooks...
/// Channel.js component ///
const Channel = ({ id, channelName, channelDescription, authorName }) => {
const chatContext = useContext(ChatContext);
const client = useApolloClient();
const { fetchChannelInfo, setCurrentChannel } = chatContext;
const selectChannel = (e, id) => {
fetchChannelInfo(true);
const currentChannel = {
channelid: id,
channelName,
channelDescription,
authorName
};
setCurrentChannel(currentChannel);
client.writeData({
data: {
channelid: id
}
});
// console.log(currentChannel);
};
return (
<ChannelNameAndLogo onClick={e => selectChannel(e, id)}>
<ChannelLogo className='fab fa-slack-hash' />
<ChannelName>{channelName}</ChannelName>
</ChannelNameAndLogo>
);
};
export default Channel;
/// Messages.js component ///
const FETCH_CHANNELID = gql`
{
channelid #client
}
`;
const Messages = () => {
const [messageContent, setMessageContent] = useState('');
const chatContext = useContext(ChatContext);
const { currentChannel } = chatContext;
// const { data, loading, refetch } = useQuery(FETCH_MESSAGES, {
// skip: true
// });
const { data: channelidData, loading: channelidLoading } = useQuery(
FETCH_CHANNELID
);
const [fetchMessages, { data, called, loading, error }] = useLazyQuery(
FETCH_MESSAGES
);
//// useMutation is working
const [
createMessage,
{ data: MessageData, loading: MessageLoading }
] = useMutation(CREATE_MESSAGE);
if (channelidLoading && !channelidData) {
console.log('loading');
setInterval(() => {
console.log('loading ...');
}, 1000);
} else if (!channelidLoading && channelidData) {
console.log('not loading anymore...');
console.log(channelidData.channelid);
fetchMessages({ variables: { channelid: channelidData.channelid } });
console.log(data);
}
I expect to have messages in data from the useLazyQuery ...But instead get this in the console.log():
react-dom.development.js:16408 Uncaught Invariant Violation: Too many re-renders. React limits the number of renders to prevent an infinite loop.
You could use the called variable return by useLazyQuery.
!called && fetchMessages({ variables: { channelid: channelidData.channelid } });
You call fetchMessages on every render.
Try to put fetchMessages in a useEffect :
useEffect(() => {
if (!channelidLoading && channelidData) {
fetchMessages();
}
}, [channelidLoading, channelidData]);
Like that the fetchMessages function only calls when
channelidLoading or channelidData is changing.
You could also look at doing the following:
import debounce from 'lodash.debounce';
...
const [fetchMessages, { data, called, loading, error }] = useLazyQuery(
FETCH_MESSAGES
);
const findMessageButChill = debounce(fetchMessages, 350);
...
} else if (!channelidLoading && channelidData) {
findMessageButChill({
variables: { channelid: channelidData.channelid },
});
}

Resources