Next js: Hydration failed with React Query and React Cookie - reactjs

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;
};

Related

Pausing react query and re-fetching new data

I have a useQuery which is disabled in a react function component. I have another useQuery that uses mutate and on the success it calls refetchMovies(). This all seems to work well but I'm seeing old data in the refetchMovies. Is there a way for to get the refetchMovies to always call fresh data from the server when its called ?
const MyComponent = () => {
const {data, refetch: refetchMovies} = useQuery('movies', fetchMovies, {
query: {
enabled: false
}
})
const {mutate} = useQuery('list', fetchList)
const addList = useCallback(
(data) => {
mutate(
{
data: {
collection: data,
},
},
{
onSuccess: () => refetchMovies(),
onError: (error) => console.log('error')
}
)
},
[mutate, refetchMovies]
)
return (
<div onClick={addList}> {data} </div>
)
}
Try to invalidate the query in your onSuccess callback instead of manually refetching it:
https://tanstack.com/query/v4/docs/react/guides/query-invalidation
Example:
// Invalidate every query with a key that starts with `todos`
queryClient.invalidateQueries({ queryKey: ['todos'] })

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

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.

SWR hook not reflecting database change

This component is for counting views at page level in Next.js app deployed on AWS Lambda
function ViewsCounter({ slug }: { slug: string }) {
const { data } = useSWR(`/api/views/${slug}`, fetcher);
const views = new Number(data?.total);
useEffect(() => {
const registerView = () =>
fetch(`/api/views/${slug}`, { method: "POST" })
.catch(console.log);
registerView();
}, [slug]);
return (
<>
{views}
</>
);
}
This one is for displaying views on homepage
function ViewsDisplay({ slug }: { slug: string }) {
const { data } = useSWR(`/api/views/${slug}`, fetcher);
const views = new Number(data?.total);
return (
<>
{views}
</>
);
}
While it works as expected on localhost, looks like it displays only the first fetched value and doesn't revalidate it for some reason.
When visiting the page, Counter is triggered correctly and the value is changed in DB.
Probably it has something to do with mutating, any hints are appreciated.
useSWR won't automatically refetch data by default.
You can either enable automatic refetch using the refreshInterval option.
const { data } = useSWR(`/api/views/${slug}`, fetcher, { refreshInterval: 1000 });
Or explicitly update the data yourself using a mutation after the POST request to the API.
function ViewsCounter({ slug }: { slug: string }) {
const { data, mutate } = useSWR(`/api/views/${slug}`, fetcher);
const views = new Number(data?.total);
useEffect(() => {
const registerView = () =>
fetch(`/api/views/${slug}`, { method: "POST" })
.then(() => {
mutate();
})
.catch(console.log);
registerView();
}, [slug]);
return (<>{views}</>);
}

How do I make a query in useEffect() to avoid InvalidHookError?

I'm trying to query an api to get user permissions AFTER logging the user in.
But InvalidHookError occurs if I write the useQuery() inside useEffect() as it break React's rules of hooks.
const OnHeader = () => {
const [user, loading, error] =
typeof window !== "undefined" ? useAuthState(firebase.auth()) : [null, true, null]
useEffect(() => {
if (user) {
user.getIdToken().then(idToken => {
localStorage.setItem("accessToken", idToken)
})
// todo: query permissions and set them in localstorage
// but if I put useQuery() here it breaks the rules
}
}, [user])
}
My current workaround is using another constant userLoggedIn to detect if a user is logged in. But I'm wondering if there's a better way to write this?
const OnHeader = () => {
const [user, loading, error] =
typeof window !== "undefined" ? useAuthState(firebase.auth()) : [null, true, null]
const [userLoggedIn, setUserLoggedIn] = useState(false)
var p = useQuery(
gql`
query QueryPermissions {
permissions {
action
isPermitted
}
}
`,
{
skip: !userLoggedIn,
onCompleted: data => {
localStorage.setItem("permissions", JSON.stringify(data))
},
}
)
useEffect(() => {
if (user) {
user.getIdToken().then(idToken => {
localStorage.setItem("accessToken", idToken)
setUserLoggedIn(true)
})
}
}, [user])
}
The problem is not in the useEffect but in your call to useAuthState. You're breaking the rules of hooks by calling a hook conditionally which is a no-no. Remove the conditional call and put the default values into your custom hook.

React Hooks - How to test changes on global providers

I'm trying to test the following scenario:
A user with an expired token tries to access a resource he is not authorized
The resources returns a 401 error
The application updates a global state "isExpiredSession" to true
For this, I have 2 providers:
The authentication provider, with the global authentication state
The one responsible to fetch the resource
There are custom hooks for both, exposing shared logic of these components, i.e: fetchResource/expireSesssion
When the resource fetched returns a 401 status, it sets the isExpiredSession value in the authentication provider, through the sharing of a setState method.
AuthenticationContext.js
import React, { createContext, useState } from 'react';
const AuthenticationContext = createContext([{}, () => {}]);
const initialState = {
userInfo: null,
errorMessage: null,
isExpiredSession: false,
};
const AuthenticationProvider = ({ authStateTest, children }) => {
const [authState, setAuthState] = useState(initialState);
return (
<AuthenticationContext.Provider value={[authStateTest || authState, setAuthState]}>
{ children }
</AuthenticationContext.Provider>);
};
export { AuthenticationContext, AuthenticationProvider, initialState };
useAuthentication.js
import { AuthenticationContext, initialState } from './AuthenticationContext';
const useAuthentication = () => {
const [authState, setAuthState] = useContext(AuthenticationContext);
...
const expireSession = () => {
setAuthState({
...authState,
isExpiredSession: true,
});
};
...
return { expireSession };
}
ResourceContext.js is similar to the authentication, exposing a Provider
And the useResource.js has something like this:
const useResource = () => {
const [resourceState, setResourceState] = useContext(ResourceContext);
const [authState, setAuthState] = useContext(AuthenticationContext);
const { expireSession } = useAuthentication();
const getResource = () => {
const { values } = resourceState;
const { userInfo } = authState;
return MyService.fetchResource(userInfo.token)
.then((result) => {
if (result.ok) {
result.json()
.then((json) => {
setResourceState({
...resourceState,
values: json,
});
})
.catch((error) => {
setErrorMessage(`Error decoding response: ${error.message}`);
});
} else {
const errorMessage = result.status === 401 ?
'Your session is expired, please login again' :
'Error retrieving earnings';
setErrorMessage(errorMessage);
expireSession();
}
})
.catch((error) => {
setErrorMessage(error.message);
});
};
...
Then, on my tests, using react-hooks-testing-library I do the following:
it.only('Should fail to get resource with invalid session', async () => {
const wrapper = ({ children }) => (
<AuthenticationProvider authStateTest={{ userInfo: { token: 'FOOBAR' }, isExpiredSession: false }}>
<ResourceProvider>{children}</ResourceProvider>
</AuthenticationProvider>
);
const { result, waitForNextUpdate } = renderHook(() => useResource(), { wrapper });
fetch.mockResponse(JSON.stringify({}), { status: 401 });
act(() => result.current.getResource());
await waitForNextUpdate();
expect(result.current.errorMessage).toEqual('Your session is expired, please login again');
// Here is the issue, how to test the global value of the Authentication context? the line below, of course, doesn't work
expect(result.current.isExpiredSession).toBeTruthy();
});
I have tried a few solutions:
Rendering the useAuthentication on the tests as well, however, the changes made by the Resource doesn't seem to reflect on it.
Exposing the isExpiredSession variable through the Resource hook, i.e:
return {
...
isExpiredSession: authState.isExpiredSession,
...
};
I was expecting that by then this line would work:
expect(result.current.isExpiredSession).toBeTruthy();
But still not working and the value is still false
Any idea how can I implement a solution for this problem?
Author of react-hooks-testing-library here.
It's a bit hard without being able to run the code, but I think your issue might be the multiple state updates not batching correctly as they are not wrapped in an act call. The ability to act on async calls is in an alpha release of react (v16.9.0-alpha.0) and we have an issue tracking it as well.
So there may be 2 ways to solve it:
Update to the alpha version and a move the waitForNextUpdate into the act callback
npm install react#16.9.0-alpha.0
it.only('Should fail to get resource with invalid session', async () => {
const wrapper = ({ children }) => (
<AuthenticationProvider authStateTest={{ userInfo: { token: 'FOOBAR' }, isExpiredSession: false }}>
<ResourceProvider>{children}</ResourceProvider>
</AuthenticationProvider>
);
const { result, waitForNextUpdate } = renderHook(() => useResource(), { wrapper });
fetch.mockResponse(JSON.stringify({}), { status: 401 });
await act(async () => {
result.current.getResource();
await waitForNextUpdate();
});
expect(result.current.errorMessage).toEqual('Your session is expired, please login again');
expect(result.current.isExpiredSession).toBeTruthy();
});
Add in a second waitForNextUpdate call
it.only('Should fail to get resource with invalid session', async () => {
const wrapper = ({ children }) => (
<AuthenticationProvider authStateTest={{ userInfo: { token: 'FOOBAR' }, isExpiredSession: false }}>
<ResourceProvider>{children}</ResourceProvider>
</AuthenticationProvider>
);
const { result, waitForNextUpdate } = renderHook(() => useResource(), { wrapper });
fetch.mockResponse(JSON.stringify({}), { status: 401 });
act(() => result.current.getResource());
// await setErrorMessage to happen
await waitForNextUpdate();
// await setAuthState to happen
await waitForNextUpdate();
expect(result.current.errorMessage).toEqual('Your session is expired, please login again');
expect(result.current.isExpiredSession).toBeTruthy();
});
Your appetite for using alpha versions will likely dictate which option you go for, but, option 1 is the more "future proof". Option 2 may stop working one day once the alpha version hits a stable release.

Resources