How to manage global states with React Query - reactjs

i have a project that's using Nextjs and Supabase. I was using context API and now i'm trying to replace it for React Query, but i'm having a hard time doing it. First of all, can i replace context completely by React Query?
I created this hook to get the current user
export const getUser = async (): Promise<Profile> => {
const onFetch = await supabase.auth.getUser();
const userId = onFetch.data.user?.id;
let { data, error } = await supabase
.from("profiles")
.select()
.eq("id", userId)
.single();
return data;
};
export const useUser = () => {
return useQuery(["user"], () => getUser());
};
I'm not sure how to trigger it. When i was using context i was getting the user as this. If data, then it would redirect to the HomePage
let { data, error, status } = await supabase
.from("profiles")
.select()
.eq("id", id)
.single();
if (data) {
setUser(data);
return true;
}
Since i was getting the user before redirecting to any page, when i navigated to profile page, the user was already defined. How can i get the user before anything and keep this state? I suppose that once the user is already defined, at the profile component i can call useUser and just use it's data. But it's giving me undefined when i navigate to profile, i suppose that it's fetching again.
const { data, isLoading } = useUser();

But it's giving me undefined when i navigate to profile, i suppose that it's fetching again.
Once data is fetched when you call useUser, it will not be removed anymore (unless it can be garbage collected after it has been unused for some time). So if you do a client side navigation (that is not a full page reload) to another route, and you call useUser there again, you should get data back immediately, potentially with a background refetch, depending on your staleTime setting).
If you're still getting undefined, one likely error is that you are creating your QueryClient inside your app and it thus gets re-created, throwing the previous cache away. You're not showing how you do that so it's hard to say. Maybe have a look at these FAQs: https://tkdodo.eu/blog/react-query-fa-qs#2-the-queryclient-is-not-stable

Related

Using SWR Bound Mutation Across Nextjs Pages with useSWRInfinite

Need help on using mutation with useSWRInfinite
EXPECTED
On my posts page, I used useSWRInfinite to do load posts with pagination. it provides me a bounded mutation which I can use on that page
The user is then able to select the post and go to a postDetails page where they can see details of the post and delete it
On the postDetails page, I want to allow the user to delete a post which will open a modal asking him to confirm delete. Once the user clicks on confirm delete, the post will be deleted via an axios.delete call and then the posts will be mutated and user will be sent back to the posts page. the new paginated data will no longer show the deleted post
PROBLEM
The delete function is where the problem lies. When i use my usePagination hook to get the bounded mutate function which I call after deleting the data, I get a key conflict error. I also tried doing mutate with the key to my api but it doesn't update the cache.
QUESTION
Is there anyway I can use the bounded mutation across nextjs pages? or is there something else I should do to make my delete function works? Thanks
// todo
const onDelete = async (postId: string) => {
await axios.delete(`/api/demo/posts/${postId}`);
//mutate(`/api/demo/posts`); - doesn't mutate the data
mutatePosts(); // mutates the data but gets conflicting keys
replace("/demo/posts");
};
The mutate from calling the usePagination function will lead to key conflict error
const { mutate: mutatePosts } = usePagination<IPost>(`/api/demo/posts`);
Using the mutate from useSWRConfig() and passing in the api key '/api/demo/posts' will not update the cache at all.
const { mutate } = useSWRConfig();
I used usePagination which is a custom hook to make the api call in posts page. However, I am unable to use the bounded mutate here across nextjs pages in the postDetail page
const {
paginatedData: paginatedPosts,
error,
isLoadingMore,
isReachedEnd,
setPage,
page,
mutate: mutatePosts,
} = usePagination<IPost>(`/api/demo/posts`, searchText);
usePagination custom hook
import useSWRInfinite from "swr/infinite";
export const usePagination = <T>(url: string, searchText: string = "") => {
const PAGE_SIZE = 2;
const getKey = (pageIndex: number, previousPageData: T[]) => {
pageIndex = pageIndex + 1;
if (previousPageData && !previousPageData.length) return null; // reached the end
return `${url}?page=${pageIndex}&limit=${PAGE_SIZE}&searchText=${searchText}`;
};
const {
data,
size: page,
setSize: setPage,
error,
isValidating,
mutate,
} = useSWRInfinite(getKey);
const paginatedData: T[] = [].concat.apply([], data!);
const isLoadingMore = data && typeof data[page - 1] === "undefined";
const isReachedEnd = data && data[data.length - 1]?.length < PAGE_SIZE;
return {
paginatedData,
isLoadingMore,
isReachedEnd,
page,
setPage,
isValidating,
error,
mutate,
};
};

How to use zustand to store the result of a query

I want to put the authenticated user in a zustand store. I get the authenticated user using react-query and that causes some problems. I'm not sure why I'm doing this. I want everything related to authentication can be accessed in a hook, so I thought zustand was a good choice.
This is the hook that fetches auth user:
const getAuthUser = async () => {
const { data } = await axios.get<AuthUserResponse>(`/auth/me`, {
withCredentials: true,
});
return data.user;
};
export const useAuthUserQuery = () => {
return useQuery("auth-user", getAuthUser);
};
And I want to put auth user in this store:
export const useAuthStore = create(() => ({
authUser: useAuthUserQuery(),
}));
This is the error that I get:
Error: Invalid hook call. Hooks can only be called inside of the body
of a function component. This could happen for one of the following
reasons.
you can read about it in the react documentation:
https://reactjs.org/warnings/invalid-hook-call-warning.html
(I changed the name of some functions in this post for the sake of understandability. useMeQuery = useAuthUserQuery)
I understand the error but I don't know how to fix it.
The misunderstanding here is that you don’t need to put data from react query into any other state management solution. React query is in itself a global state manager. You can just do:
const { data } = useAuthUserQuery()
in every component that needs the data. React query will automatically try to keep your data updated with background refetches. If you don’t need that for your resource, consider setting a staleTime.
—-
That being said, if you really want to put data from react-query into zustand, create a setter in zustand and call it in the onSuccess callback of the query:
useQuery(key, queryFn, { onSuccess: data => setToZustand(data) })

How to show loading state only if data is not received yet. Next.js ssr

I have multiple getServerSideProps in my project and I have a header which displays pages and I have to wait for a page to be opened once I click upon it since I need data to be fetched. Once they are fetched the page will be open.
One approach I used to show user a loading state is to use routeChangeStart BUT I stumbled upon one problem and so I would like not to use this case.
If I go on a page and the data is fetching I want to show user a spinner or some indicator and once the data is fetched I want to stop the indicator/spinner.
As you probably figured out, getServerSideProps runs on the server and is blocking. The fetch request needs to complete before the HTML is sent to the user (i.e., the page is changed). So if you want to show a loading indicator, you need to move that fetch request to the client.
For instance, if you probably have a page with this basic structure:
export default function Page({ data }) {
return <div>{data.name}</div>
}
export async function getServerSideProps() {
const response = await fetch('https://example.com/api')
const data = await response.json()
return {
props: { data },
}
}
const fetcher = url => fetch(url).then(res => res.json());
export default function Page() {
const { data } = useSWR('https://example.com/api', fetcher)
if (!data) return <LoadingSpinner />
return <div>{data.name}</div>
}
Or if you don't need SWR and can use a simple fetch request:
export default function Page() {
const [data, setData] = useState()
useEffect(() => {
fetch('https://example.com/api')
.then(async(response) => {
const json = await response.json()
setData(json)
})
})
if (!data) return <LoadingSpinner />
return <div>{data.name}</div>
}
P.S. If the initial fetch request in getServerSideProps used sensitive information (e.g., API secret credentials), then go ahead and setup a Next.js API route to handle the sensitive part and then fetch the new route.
I just used routeChangeStart.
I didn't want to use it since router.push('/map') didn't work in pages/index.tsx file but I solved this issue by creating a new component putting router.push in useeffect and rendering a loader.
routeChangeStart was in _app.js and because of this in index.js router.push() didn't work - I tested it
routeChangeStart - how it works?
When we click on a page the data is being fetched on the server and the page will only be displayed to us once the data is fetched. So we can make the next thing, we can just intercept the route change.
When we click on a link(we wait for data to fetch) we set loading state in routeChangeStart to true and if we moved to another page(it means we fetched the data) we invoke routeChangeComplete which runs once we moved to the route we wanted to, and here we set loading state to false. And after this I just pass the loading state using React Context

How can I stay the user in the same page?

Every time I reload the my account page, it will go to the log in page for a while and will directed to the Logged in Homepage. How can I stay on the same even after refreshing the page?
I'm just practicing reactjs and I think this is the code that's causing this redirecting to log-in then to home
//if the currentUser is signed in in the application
export const getCurrentUser = () => {
return new Promise((resolve, reject) => {
const unsubscribe = auth.onAuthStateChanged(userAuth => {
unsubscribe();
resolve(userAuth); //this tell us if the user is signed in with the application or not
}, reject);
})
};
.....
import {useEffect} from 'react';
import { useSelector } from 'react-redux';
const mapState = ({ user }) => ({
currentUser: user.currentUser
});
//custom hook
const useAuth = props => {
//get that value, if the current user is null, meaning the user is not logged in
// if they want to access the page, they need to be redirected in a way to log in
const { currentUser } = useSelector(mapState);
useEffect(() => {
//checks if the current user is null
if(!currentUser){
//redirect the user to the log in page
//we have access to history because of withRoute in withAuth.js
props.history.push('/login');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},[currentUser]); //whenever currentUser changes, it will run this code
return currentUser;
};
export default useAuth;
You can make use of local storage as previously mentioned in the comments:
When user logs in
localStorage.setItem('currentUserLogged', true);
And before if(!currentUser)
var currentUser = localStorage.getItem('currentUserLogged');
Please have a look into the following example
Otherwise I recommend you to take a look into Redux Subscribers where you can persist states like so:
store.subscribe(() => {
// store state
})
There are two ways through which you can authenticate your application by using local storage.
The first one is :
set a token value in local storage at the time of logging into your application
localStorage.setItem("auth_token", "any random generated token or any value");
you can use the componentDidMount() method. This method runs on the mounting of any component. you can check here if the value stored in local storage is present or not if it is present it means the user is logged in and can access your page and if not you can redirect the user to the login page.
componentDidMount = () => { if(!localStorage.getItem("auth_token")){ // redirect to login page } }
The second option to authenticate your application by making guards. You can create auth-guards and integrate those guards on your routes. Those guards will check the requirement before rendering each route. It will make your code clean and you do not need to put auth check on every component like the first option.
There are many other ways too for eg. if you are using redux you can use Persist storage or redux store for storing the value but more secure and easy to use.

React query how to handle a search

I'm just playing around with react query
I'm building sort of a simple github clone
I have to use useQuery twice one for the current user
as router param the other with a new search.
I ended up with:
const history = useHistory();
const currentUser: string = useRouterPathname();
const [user, setUser] = useState(currentUser);
const handleFormSubmit = (data: SearchFormInputs) => {
history.push(`/${data.search}`);
setUser(data.search);
};
const { isLoading, error, data } = useGetUserData(user);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>An error has occurred: " + {error}</p>;
console.log(user, data);
Is it the right way?
The important part is probably that in useGetUserData, you put the user into the queryKey of react-query, something like:
const useGetUserData = (user) => useQuery(['users', user], () => fetchUserData(user))
so that you always refetch data when the user changes and users are not sharing a cache entry between them.
Something not react-query related though: The good thing about react-router is that you can also use it as a state manager - their hooks also subscribe you to changes, so there is no real need to duplicate that with local state:
const history = useHistory();
const currentUser: string = useRouterPathname();
const handleFormSubmit = (data: SearchFormInputs) => {
history.push(`/${data.search}`);
};
const { isLoading, error, data } = useGetUserData(currentUser);
once you push to the history, all subscribers (via useParams or useLocation) will also re-render, thus giving you a new currentUser.
Lastly, I would recommend checking for data availability first rather than for loading/error:
const { error, data } = useGetUserData(user);
if (data) return renderTheData
if (error) return <p>An error has occurred: " + {error}</p>;
return <p>Loading...</p>
it obviously depends on your use-case, but generally, if a background refetch happens, and it errors, we still have data (albeit stale data) that we can show to the user instead. It's mostly unexpected if you see data on the screen, refocus your browser tab (react-query will try to update the data in the background then per default), and then the data disappears and the error is shown. It might be relevant in some cases, but most people are not aware that you can have data and error at the same time, and you have to prioritise for one or the other.

Resources