React-query tips and tricks with Github graphQL and REST api
Tags: reactquery, react, graphql, typescript
Published: 2022-09-02 18:12:31

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

```js
const fetchdatas=async()=>{
const token ="token token token "
const url="https://api.github.com/user"
   const res = await axios({
        method: 'get',
        url: url,
        headers: {
            Authorization: `Bearer ${token}`,
            "Content-Type": "application/json"
        },
    })
    // //console.log("response == ",res)

    const user = res.data 
    // //console.log("authed user == ",user)
    return user
}
```
**graphql api**
i'll use graphql-request

```js
 const USER = gql`
         query getMiniUser($name: String!) {
           user(login: $name) {
             login
             id
             isFollowingViewer
             viewerIsFollowing
             bio
             avatarUrl
             isViewer
             url
           }
         }
       `;
const graphQLClient = new GraphQLClient(endpoint, headers);
const fetchData = async () => await graphQLClient.request(USER,{name:"tigawanna);
```
**step 2**:connect it to a query hook in the case useQuery

```js
const key =['user']
useQuery(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 

```js
const key =['user',name_variable]
useQuery(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

```js
const key =['user']
useQuery(key, fetchData,
{
enabled:name_variable !== "" && name_variable.length > 3
);
```

- 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

```js
const query = useQuery(key, fetchData,
{
enabled: username.length > 3,
  select: (data:User) => {
 let newdata   
  if (data.user.isViewer){
  newdata = {...data.user,OP:true}
  return {user:newdata}
  }
  return data
  }
}
)
```
the select option can also be used to reorder items , filter down an array of items without fetching the data

```ts
const keyword = "a"
const query = useQuery(key, fetchData,
{
enabled: username.length > 3,
  select: (data:User) => {
 let newdata   
  if (keyword){
  const followers = data.user.followers.edges.filter((item)=>{
   return item.node.login.includes(keyword)
  })  
  newdata = {...data.user,followers}
  return {user:newdata}
  }
  return data
  }
}
)
```

- 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

```ts
const query = useQuery(key, fetchData,
{
enabled: username.length > 3,
  select: (data:User) => {
 let newdata   
  if (keyword){
  const followers = data.user.followers.edges.filter((item)=>{
   return item.node.login.includes(keyword)
  })  
  newdata = {...data.user,followers}
  return {user:newdata}
  }
  return data
  },
  onSuccess:(data:User)=>{
    console.log("success")
//save token to local storage no issues with it
  },
  onError:(error:any)=>{
    console.log("error  = ",error.response)
    if(error?.response?.status === "401" || error?.response?.status === "402"){
      // invalidate my locally stored token to send me back to 
      // login screen,those codes mean tokenhas an issue
    }
  }
}
)
```

**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

```js
const query = useInfiniteQuery(key, fetchData, {
  enabled: username.length > 3,
 onSuccess: (data: RootResponse) => {
    console.log("success", data);
  },
  onError: (error: any) => {
    console.log("error  = ", error.response);
    if (error?.response?.status === "401" || error?.response?.status === "402") {
      // invalidate my locally stored token to send me back to
      // login screen,those codes mean tokenhas an issue
    }
  },
  getPreviousPageParam: (firstPage: Page) => {
    return firstPage?.user?.followers?.pageInfo?.startCursor ?? null;
  },
  getNextPageParam: (lastPage: Page) => {
    return lastPage?.user?.followers?.pageInfo?.endCursor ?? null;
  },
});
console.log("test query === ",query.data)
```
and add a load more button

```js
  const pages = query?.data?.pages;
  //  console.log("followers === ",followers)
  //@ts-ignore
  const extras = pages[pages.length - 1].user?.followers;
  const hasMore = extras?.pageInfo?.hasNextPage;

return (
  <div className="w-full h-full ">
    {!query.isFetchingNextPage && hasMore? (
      <button
        className="m-2 hover:text-purple-400 shadow-lg hover:shadow-purple"
        onClick={() => {
          query.fetchNextPage();
        }}
      >
        --- load more ---
      </button>
    ) : null}
    {query.isFetchingNextPage ? (
      <div className="w-full flex-center m-1 p-1">loading more...</div>
    ) : null}
  </div>
)

```
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

```js
    <div className="h-fit w-full flex-center  flex-wrap">
      {pages?.map((page) => {
        return page?.user?.followers?.edges?.map((item) => {
          return (
            <div className='w-[30%] h-14 p-3 m-2 border border-green-500 '>
              user :{item.node.login}</div>
          );
        });
      })}
    </div>

```


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 

```ts
const fetchData = async () => await graphQLClient.request(USER,{name:"tigawanna",
first:10,after:null
});

```
into 

```ts
const fetchData = async (deps: any) => {
    const after = deps?.pageParam ? deps.pageParam : null;
    return await graphQLClient.request(USER, {
      name: "tigawanna",
      first: 10,
      after,
    });
  };

```
And we'll grab it from deps which will be receiving its value from 

```ts
  getNextPageParam: (lastPage: Page) => {
    return lastPage?.user?.followers?.pageInfo?.endCursor ?? null;
  },

```



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

```ts
export const concatFollowerPages = (data: RootResponse, keyword: string) => {

let totalRepos = data.pages[0].user.followers.edges;
  let i = 1;
  for (i = 1; i < data.pages.length; i++) {
    if (data?.pages) {
      totalRepos = [...totalRepos, ...data.pages[i].user.followers.edges];
    }
  }

  const filtered = totalRepos.filter((item) =>
    item.node.login.toLowerCase().includes(keyword.toLowerCase())
  );
  const base = data.pages[data.pages.length -1].user;
  const user = {
    ...base,
    login: base.login,
    followers: {
      edges: filtered,
      totalCount: base.followers.totalCount,
      pageInfo: base.followers.pageInfo,
    },
  };

  const final:RootResponse = {
    pageParams: [...data.pageParams],
    pages: [{ user: user }],
  };

  return final;
};

```

then we'll call it inside select

```js
const query = useInfiniteQuery(key, fetchData, {
  enabled: username.length > 3,
  select:(data:RootResponse)=>{
  return concatFollowerPages(data,"")
  },
  onSuccess: (data: RootResponse) => {
    // console.log("success", data);
  },
  onError: (error: any) => {
    console.log("error  = ", error.response);
    if (error?.response?.status === "401" || error?.response?.status === "402") {
      // invalidate my locally stored token to send me back to
      // login screen,those codes mean tokenhas an issue
    }
  },
  getPreviousPageParam: (firstPage: Page) => {
    return firstPage?.user?.followers?.pageInfo?.startCursor ?? null;
  },
  getNextPageParam: (lastPage: Page) => {
    return lastPage?.user?.followers?.pageInfo?.endCursor ?? null;
  },
});
```
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
 %}