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
to start the dev server
npm run deploy
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
usefull links
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 + typescript + tailwindcss
- react-query : react-query-tips and tricks
- graphql + graphql-request + graphql-tag
- dayjs
- github graphql api
- custom hooks :useLoaclStorage
- react-icons : custom icon wrapper
- tailwindcss custom loading screen
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
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
}
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);
step 2:connect it to a query hook in the case useQuery
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
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
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
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
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
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
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
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
<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
const fetchData = async () => await graphQLClient.request(USER,{name:"tigawanna",
first:10,after:null
});
into
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
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
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
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
Top comments (0)