One of the good things about React's opinionated nature is that it's allowed the community to come up with some brilliant solutions and one of them is react-query now going as tanstack-query because they've expanded to the other frameworks
Should you use react-query , I'd say probably . if your ap is depends on making a lot of network requests you should consider it before reaching for something like redux
bear in mind that react query is opinionated about how you fetch your data , its job is handling the response and error/loading states plus cache and retries
Full code linked below , example usage in a project {% embed https://github.com/tigawanna/gitpals %} {% embed https://github.com/tigawanna/gitdeck %}
initialization is simple step 1 : create the actual async query function which returns a response or error
rest api i'll use axios , you can use fetch too
1const fetchdatas=async()=>{
2const token ="token token token "
3const url="https://api.github.com/user"
4 const res = await axios({
5 method: 'get',
6 url: url,
7 headers: {
8 Authorization: `Bearer ${token}`,
9 "Content-Type": "application/json"
10 },
11 })
12
13
14 const user = res.data
15
16 return user
17}
graphql api i'll use graphql-request
1 const USER = gql`
2 query getMiniUser($name: String!) {
3 user(login: $name) {
4 login
5 id
6 isFollowingViewer
7 viewerIsFollowing
8 bio
9 avatarUrl
10 isViewer
11 url
12 }
13 }
14 `;
15const graphQLClient = new GraphQLClient(endpoint, headers);
16const fetchData = async () => await graphQLClient.request(USER,{name:"tigawanna);
step 2:connect it to a query hook in the case useQuery
1const key =['user']
2useQuery(key, fetchData,);
here are some tips and tricks i keep reaching for in most of my projects
- 1 the key the react-quryke is an array, the first item will be a unique string and can also add any variable that the query depends for example if you want the query to re run if the name variable changes
1const key =['user',name_variable]
2useQuery(key, fetchData);
- 2 the enabled option : it's very common to want a query to only run after a certain condition is met to avoid unnecessary queries
1const key =['user']
2useQuery(key, fetchData,
3{
4enabled:name_variable !== "" && name_variable.length > 3
5);
- 3 the select option : is a function with access to the returned data and allows you to modify the data on the client side before rendering
1const query = useQuery(key, fetchData,
2{
3enabled: username.length > 3,
4 select: (data:User) => {
5 let newdata
6 if (data.user.isViewer){
7 newdata = {...data.user,OP:true}
8 return {user:newdata}
9 }
10 return data
11 }
12}
13)
the select option can also be used to reorder items , filter down an array of items without fetching the data
1const keyword = "a"
2const query = useQuery(key, fetchData,
3{
4enabled: username.length > 3,
5 select: (data:User) => {
6 let newdata
7 if (keyword){
8 const followers = data.user.followers.edges.filter((item)=>{
9 return item.node.login.includes(keyword)
10 })
11 newdata = {...data.user,followers}
12 return {user:newdata}
13 }
14 return data
15 }
16}
17)
- 4 onError and onSuccess side effects : these functions get called when these events happen, i used it to automatically invalidate the github personal access token if the error code was 401 or 402 and save it to local storage if no issue occurred inside the onSuccess
1const query = useQuery(key, fetchData,
2{
3enabled: username.length > 3,
4 select: (data:User) => {
5 let newdata
6 if (keyword){
7 const followers = data.user.followers.edges.filter((item)=>{
8 return item.node.login.includes(keyword)
9 })
10 newdata = {...data.user,followers}
11 return {user:newdata}
12 }
13 return data
14 },
15 onSuccess:(data:User)=>{
16 console.log("success")
17
18 },
19 onError:(error:any)=>{
20 console.log("error = ",error.response)
21 if(error?.response?.status === "401" || error?.response?.status === "402"){
22
23
24 }
25 }
26}
27)
BONUS TIP : useInfiniteQuery hook
useInfiniteQuery hook is for handling pagination of long lists of data. For example in the graphql query we can pass in first and after params to a field like followers which could have a very long list
so we introduce pagination like so
1const query = useInfiniteQuery(key, fetchData, {
2 enabled: username.length > 3,
3 onSuccess: (data: RootResponse) => {
4 console.log("success", data);
5 },
6 onError: (error: any) => {
7 console.log("error = ", error.response);
8 if (error?.response?.status === "401" || error?.response?.status === "402") {
9
10
11 }
12 },
13 getPreviousPageParam: (firstPage: Page) => {
14 return firstPage?.user?.followers?.pageInfo?.startCursor ?? null;
15 },
16 getNextPageParam: (lastPage: Page) => {
17 return lastPage?.user?.followers?.pageInfo?.endCursor ?? null;
18 },
19});
20console.log("test query === ",query.data)
and add a load more button
1 const pages = query?.data?.pages;
2
3
4 const extras = pages[pages.length - 1].user?.followers;
5 const hasMore = extras?.pageInfo?.hasNextPage;
6
7return (
8 <div className="w-full h-full ">
9 {!query.isFetchingNextPage && hasMore? (
10 <button
11 className="m-2 hover:text-purple-400 shadow-lg hover:shadow-purple"
12 onClick={() => {
13 query.fetchNextPage();
14 }}
15 >
16 --- load more ---
17 </button>
18 ) : null}
19 {query.isFetchingNextPage ? (
20 <div className="w-full flex-center m-1 p-1">loading more...</div>
21 ) : null}
22 </div>
23)
24
by default react query will return the data as a pages arra that will add a new array into the pages array and that will require us to use nested loops to output the data
1 <div className="h-fit w-full flex-center flex-wrap">
2 {pages?.map((page) => {
3 return page?.user?.followers?.edges?.map((item) => {
4 return (
5 <div className='w-[30%] h-14 p-3 m-2 border border-green-500 '>
6 user :{item.node.login}</div>
7 );
8 });
9 })}
10 </div>
11
Also note the side effect functions provided by react query to handle the pagination getNextPageParam and getPreviousPageParam injects the value in this case the end cursor that the api gives us inside the pageInfo fields into the query function passed in.
in this case we'll change
1const fetchData = async () => await graphQLClient.request(USER,{name:"tigawanna",
2first:10,after:null
3});
4
into
1const fetchData = async (deps: any) => {
2 const after = deps?.pageParam ? deps.pageParam : null;
3 return await graphQLClient.request(USER, {
4 name: "tigawanna",
5 first: 10,
6 after,
7 });
8 };
9
And we'll grab it from deps which will be receiving its value from
1 getNextPageParam: (lastPage: Page) => {
2 return lastPage?.user?.followers?.pageInfo?.endCursor ?? null;
3 },
4
this works fine but if you want to filter for a certain keyword things get tricky so i made a function that transforms the data into a single array and filters any keywords provided
1export const concatFollowerPages = (data: RootResponse, keyword: string) => {
2
3let totalRepos = data.pages[0].user.followers.edges;
4 let i = 1;
5 for (i = 1; i < data.pages.length; i++) {
6 if (data?.pages) {
7 totalRepos = [...totalRepos, ...data.pages[i].user.followers.edges];
8 }
9 }
10
11 const filtered = totalRepos.filter((item) =>
12 item.node.login.toLowerCase().includes(keyword.toLowerCase())
13 );
14 const base = data.pages[data.pages.length -1].user;
15 const user = {
16 ...base,
17 login: base.login,
18 followers: {
19 edges: filtered,
20 totalCount: base.followers.totalCount,
21 pageInfo: base.followers.pageInfo,
22 },
23 };
24
25 const final:RootResponse = {
26 pageParams: [...data.pageParams],
27 pages: [{ user: user }],
28 };
29
30 return final;
31};
32
then we'll call it inside select
1const query = useInfiniteQuery(key, fetchData, {
2 enabled: username.length > 3,
3 select:(data:RootResponse)=>{
4 return concatFollowerPages(data,"")
5 },
6 onSuccess: (data: RootResponse) => {
7
8 },
9 onError: (error: any) => {
10 console.log("error = ", error.response);
11 if (error?.response?.status === "401" || error?.response?.status === "402") {
12
13
14 }
15 },
16 getPreviousPageParam: (firstPage: Page) => {
17 return firstPage?.user?.followers?.pageInfo?.startCursor ?? null;
18 },
19 getNextPageParam: (lastPage: Page) => {
20 return lastPage?.user?.followers?.pageInfo?.endCursor ?? null;
21 },
22});
For the full example code i squeezed everything into a self contained component to make it easier to make sense of
{% embed https://gist.github.com/tigawanna/32acc9552e02634609184dc14f2076a3 %}