DEV Community

Cover image for React-query tips and tricks with Github graphQL and REST api
Dennis kinuthia
Dennis kinuthia

Posted on

React-query tips and tricks with Github graphQL and REST api

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

silly little app i call gitpals

Expand your github circle and find new developers along the way

uses github rest api and react with vite /br>

scripts

npm run dev
Enter fullscreen mode Exit fullscreen mode

to start the dev server

npm run deploy
Enter fullscreen mode Exit fullscreen mode

To deploy your site to gitpages

*this requires a bunch of initial set-up

Add add your github personal access token in the field , to clear it from local storage click on the profile pic icon on the right end of the toolbar

live preview on live page link

open to colaboration

fork the repo and make a pull request if you make any progress

testing with postman





GITDECK

You'll need a personal access token to login
live preview

You multiple points of entry into the rabbit hole , either cllick on follower/ following tabs and click on a profile which wil then expose you to their repos and followers/following or just look up a username or email anf follow that one , have fun , make friends and learn something new

built with

Nextjs Specifics

This is a Next.js project bootstrapped with create-next-app.

Getting Started

First, run the development server:

npm run dev
# or
yarn dev
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:3000 with your browser to see the result.

You can start editing the page by modifying pages/index.tsx. The page auto-updates as you edit the file.

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

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
}
Enter fullscreen mode Exit fullscreen mode

graphql api
i'll use graphql-request

 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);
Enter fullscreen mode Exit fullscreen mode

step 2:connect it to a query hook in the case useQuery

const key =['user']
useQuery(key, fetchData,);
Enter fullscreen mode Exit fullscreen mode

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
const key =['user',name_variable]
useQuery(key, fetchData);
Enter fullscreen mode Exit fullscreen mode
  • 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
const key =['user']
useQuery(key, fetchData,
{
enabled:name_variable !== "" && name_variable.length > 3
);
Enter fullscreen mode Exit fullscreen mode
  • 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
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
  }
}
)
Enter fullscreen mode Exit fullscreen mode

the select option can also be used to reorder items , filter down an array of items without fetching the data

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
  }
}
)
Enter fullscreen mode Exit fullscreen mode
  • 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
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
    }
  }
}
)
Enter fullscreen mode Exit fullscreen mode

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

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)
Enter fullscreen mode Exit fullscreen mode

and add a load more button

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

Enter fullscreen mode Exit fullscreen mode

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

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

Enter fullscreen mode Exit fullscreen mode

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

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

Enter fullscreen mode Exit fullscreen mode

into

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

Enter fullscreen mode Exit fullscreen mode

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

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

Enter fullscreen mode Exit fullscreen mode

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

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

Enter fullscreen mode Exit fullscreen mode

then we'll call it inside select

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;
  },
});
Enter fullscreen mode Exit fullscreen mode

For the full example code i squeezed everything into a self contained component to make it easier to make sense of

Top comments (0)