DEV Community

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

Posted on

2

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

react-query tips and tricks i've found usefull while working with github's graphql api

using select to insert a new value into the response

import React from 'react'
import { useQuery } from 'react-query';
import { GraphQLClient } from 'graphql-request';
import { gql } from 'graphql-tag';

interface TestProps {
token:string
}

interface User{
  user:Root
}
export interface Root {
  login: string;
  id: string;
  isFollowingViewer: boolean;
  viewerIsFollowing: boolean;
  bio: string;
  avatarUrl: string;
  isViewer: boolean;
  url: string;
}

export const Test: React.FC<TestProps> = ({token}) => {  
const username = 'tigawanna'
const key =['user']

 const USER = gql`
         query getMiniUser($name: String!) {
           user(login: $name) {
             login
             id
             isFollowingViewer
             viewerIsFollowing
             bio
             avatarUrl
             isViewer
             url
           }
         }
       `;

   const endpoint = "https://api.github.com/graphql";
  const headers = {
    headers: {
      Authorization: `Bearer ${token}`,
      "Content-Type": "application/json",
    },
  };      
const graphQLClient = new GraphQLClient(endpoint, headers);
const fetchData = async () => await graphQLClient.request(USER,{name:"tigawanna"});

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

console.log("test query === ",query.data)



return (
 <div className='w-full h-full '>

HELLO
 </div>
);}

using select to filter the response array

import React from 'react'
import { useQuery } from 'react-query';
import { GraphQLClient } from 'graphql-request';
import { gql } from 'graphql-tag';

interface TestProps {
token:string
}

interface User{
  user:Root
}
export interface Followers {
  edges: Edge[];
}

export interface Edge {
  node: Node;
}

export interface Node {
  login: string;
  avatarUrl: string;
  id: string;
}
export interface Root {
  login: string;
  id: string;
  isFollowingViewer: boolean;
  viewerIsFollowing: boolean;
  bio: string;
  avatarUrl: string;
  isViewer: boolean;
  url: string;
  followers: Followers;
}

export const Test: React.FC<TestProps> = ({token}) => {  
const username = 'tigawanna'
const key =['user']

 const USER = gql`
         query getMiniUser($name: String!) {
           user(login: $name) {
             login
             id
             isFollowingViewer
             viewerIsFollowing
             bio
             avatarUrl
             isViewer
             url
          followers(first:10) {
             edges {
                 node {
                   login
                   avatarUrl
                   id
                 }
               }
               }
           }
         }
       `;

   const endpoint = "https://api.github.com/graphql";
  const headers = {
    headers: {
      Authorization: `Bearer ${token}`,
      "Content-Type": "application/json",
    },
  };      
const graphQLClient = new GraphQLClient(endpoint, headers);
const fetchData = async () => await graphQLClient.request(USER,{name:"tigawanna"});
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
  }
}
)
console.log("test query === ",query.data)



return (
 <div className='w-full h-full '>

HELLO
 </div>
);}

onError and onSuccess side effects

import React from 'react'
import { useQuery } from 'react-query';
import { GraphQLClient } from 'graphql-request';
import { gql } from 'graphql-tag';

interface TestProps {
token:string
}

interface User{
  user:Root
}
export interface Followers {
  edges: Edge[];
}

export interface Edge {
  node: Node;
}

export interface Node {
  login: string;
  avatarUrl: string;
  id: string;
}
export interface Root {
  login: string;
  id: string;
  isFollowingViewer: boolean;
  viewerIsFollowing: boolean;
  bio: string;
  avatarUrl: string;
  isViewer: boolean;
  url: string;
  followers: Followers;
}

export const Test: React.FC<TestProps> = ({token}) => {  
const username = 'tigawanna'
const key =['user']

 const USER = gql`
         query getMiniUser($name: String!) {
           user(login: $name) {
             login
             id
             isFollowingViewer
             viewerIsFollowing
             bio
             avatarUrl
             isViewer
             url
          followers(first:10) {
             edges {
                 node {
                   login
                   avatarUrl
                   id
                 }
               }
               }
           }
         }
       `;

   const endpoint = "https://api.github.com/graphql";
  const headers = {
    headers: {
      Authorization: `Bearer ${token+'gg'}`,
      "Content-Type": "application/json",
    },
  };      
const graphQLClient = new GraphQLClient(endpoint, headers);
const fetchData = async () => await graphQLClient.request(USER,{name:"tigawanna"});
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
  },
  onSuccess:(data:User)=>{
    console.log("success")
  },
  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
    }
  }
}
)
console.log("test query === ",query.data)



return (
 <div className='w-full h-full '>

HELLO
 </div>
);}

standard infinite query

import React from 'react'
import { useQuery } from 'react-query';
import { GraphQLClient } from 'graphql-request';
import { gql } from 'graphql-tag';
import { useInfiniteQuery } from 'react-query';

interface TestProps {
token:string
}


export interface RootResponse {
  pages: Page[];
  pageParams: any[];
}

export interface Page {
  user: User;
}

export interface User {
  login: string;
  id: string;
  isFollowingViewer: boolean;
  viewerIsFollowing: boolean;
  bio: string;
  avatarUrl: string;
  isViewer: boolean;
  url: string;
  followers: Followers;
}

export interface Followers {
  edges: Edge[];
  totalCount: number;
  pageInfo: PageInfo;
}

export interface Edge {
  node: Node;
}

export interface Node {
  login: string;
  avatarUrl: string;
  id: string;
}

export interface PageInfo {
  startCursor: string;
  endCursor: string;
  hasNextPage: boolean;
  hasPreviousPage: boolean;
}
export const Test: React.FC<TestProps> = ({token}) => {  
const username = 'tigawanna'
const key =['user']

 const USER = gql`
   query getMiniUser($name: String!, $first: Int!, $after: String) {
     user(login: $name) {
       login
       id
       isFollowingViewer
       viewerIsFollowing
       bio
       avatarUrl
       isViewer
       url
       followers(first: $first, after: $after) {
         edges {
           node {
             login
             avatarUrl
             id
           }
         }

         totalCount
         pageInfo {
           startCursor
           endCursor
           hasNextPage
           hasPreviousPage
         }

       }
     }
   }
 `;

   const endpoint = "https://api.github.com/graphql";
  const headers = {
    headers: {
      Authorization: `Bearer ${token}`,
      "Content-Type": "application/json",
    },
  };      
const graphQLClient = new GraphQLClient(endpoint, headers);
const fetchData = async (deps: any) => {
    const after = deps?.pageParam ? deps.pageParam : null;
    return await graphQLClient.request(USER, {
      name: "tigawanna",
      first: 10,
      after,
    });
  };

const keyword = "a"

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)

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

    {!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>
);}

better infinite query

import React from 'react'
import { useQuery } from 'react-query';
import { GraphQLClient } from 'graphql-request';
import { gql } from 'graphql-tag';
import { useInfiniteQuery } from 'react-query';

interface TestProps {
token:string
}


export interface RootResponse {
  pages: Page[];
  pageParams: any[];
}

export interface Page {
  user: User;
}

export interface User {
  login: string;
  id: string;
  isFollowingViewer: boolean;
  viewerIsFollowing: boolean;
  bio: string;
  avatarUrl: string;
  isViewer: boolean;
  url: string;
  followers: Followers;
}

export interface Followers {
  edges: Edge[];
  totalCount: number;
  pageInfo: PageInfo;
}

export interface Edge {
  node: Node;
}

export interface Node {
  login: string;
  avatarUrl: string;
  id: string;
}

export interface PageInfo {
  startCursor: string;
  endCursor: string;
  hasNextPage: boolean;
  hasPreviousPage: boolean;
}
export const Test: React.FC<TestProps> = ({token}) => {  
const username = 'tigawanna'
const key =['user']

 const USER = gql`
   query getMiniUser($name: String!, $first: Int!, $after: String) {
     user(login: $name) {
       login
       id
       isFollowingViewer
       viewerIsFollowing
       bio
       avatarUrl
       isViewer
       url
       followers(first: $first, after: $after) {
         edges {
           node {
             login
             avatarUrl
             id
           }
         }

         totalCount
         pageInfo {
           startCursor
           endCursor
           hasNextPage
           hasPreviousPage
         }

       }
     }
   }
 `;

   const endpoint = "https://api.github.com/graphql";
  const headers = {
    headers: {
      Authorization: `Bearer ${token}`,
      "Content-Type": "application/json",
    },
  };      
const graphQLClient = new GraphQLClient(endpoint, headers);
// const fetchData = async () => await graphQLClient.request(USER,{name:"tigawanna",
// first:10,after:null
// });
const fetchData = async (deps: any) => {
    const after = deps?.pageParam ? deps.pageParam : null;
    return await graphQLClient.request(USER, {
      name: "tigawanna",
      first: 10,
      after,
    });
  };


const keyword = "a"

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) => {
    console.log("end cursor ", lastPage?.user?.followers?.pageInfo?.endCursor);
    return lastPage?.user?.followers?.pageInfo?.endCursor ?? null;
  },
});
// console.log("test query === ",query.data)


if(query.isFetching){
  return <div> loading .... </div>
}

const pages = query?.data?.pages;
return (
  <div className="w-full h-full ">
    <div className="h-fit w-full flex-center  flex-wrap">
      {pages?.map((page) => {
        return page?.user?.followers?.edges?.map((item, index) => {
          return (
            <div
              key={item.node.id}
              className="w-[30%] h-14 p-3 m-2 border border-green-500 text-lg font-bold"
            >
              {index} {item.node.login}
            </div>
          );
        });
      })}
    </div>

    {!query.isFetchingNextPage &&
    query?.data?.pages[0]?.user?.followers?.pageInfo?.hasNextPage ? (
      <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>
);}




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

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read full post →

Top comments (0)

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more