Hello World! In this blog, I am going to discuss how we can use React hooks with Apollo to connect to GraphQL API in different scenarios. Assuming that you have a basic understanding of the same, I will be explaining how GraphQL data can be shared with the UI using React hooks by giving a few examples that we are already using in our ongoing project, the Litmus Portal.
#What is GraphQL?
Before going forward, let me give a very brief overview of GraphQL and what all things we are going to discuss. So GraphQL is a query language for APIs that is developed by Facebook. It is an efficient alternative to REST because of its features such as:
- With GraphQL there's no over fetching or under fetching of data, unlike REST.
- Strongly typed graphQL schema which can be written in GraphQL Schema Definition Language (SDL) helps you validate your API requests during its compile time.
- With the development of various GraphQL libraries (Apollo, Relay, etc) you are getting a lot of features such as caching, realtime data, etc.
- It provides a large and amazing community! You can always get your queries answered whenever stuck.
This was just a basic introduction to GraphQL, but I recommend you to visit the site to gain deeper insights of the same.
#What we'll do?
I will be mainly focussing on the front-end side, where I am gonna explain the two very useful react hooks useQuery
and useMutation
, how are we using these in our project for GraphQL operations along with the code.
##Wait...what are GraphQL operations?##
GraphQL provides various types of operations such as Query, Mutation, and Subscription which act as the entry points for the requests sent by the client. In this blog, I'll be discussing the first two types i.e. Query and Mutation.
- Query:
useQuery
hook is used to fetch the data from the server and attach it to the UI. To run a query you need to call this hook by passing the query string, it returns an object from Apollo client containingdata
,error
,loading
properties which change their values after execution. The hook is called when the component renders and the above properties can be used conditionally to render the UI.
Basic syntax:
const { loading, error, data } = useQuery<Type1, Type2>(
QUERY_STRING,
{ variables: <variable>,
onCompleted:()=>{console.log("query successful",data);}
,
onError:(error)=>{console.error(error);},
});
data
: The required data we are getting after the query is successful.loading
: It is a boolean value, iftrue
, it means the query is still in flight. After it is successful the value ofloading
changes tofalse
.error
: It stores the error if occurred while querying.
- Mutation:
useMutation
hook is used to send updates to the GraphQL server as a result of which data can be updated in the back-end. It is somewhat similar touseQuery
in terms of syntax with some minor differences. To execute a mutation, you need to pass the mutation string to the hook. This hook returns a tuple containing amutate
function which can be called whenever its execution is required and an object having certain fields that represent the current status of mutation's execution.
Basic syntax:
const [mutateFunction,{ error,loading}] = useMutation<Type>(MUTATION_STRING, {
onCompleted: () => {
console.log("details updated")
},
onError: (error) => {
onError:(error)=>console.error(error);
},
refetchQueries: [{ query: QUERY_STRING, variables: <variable>],
});
mutateFunction
: It is themutate
function which can be called anytime to run the mutation.- The second parameter is the object representing the mutation's execution status such as
error
,loading
which have been explained above.
In both examples, I have added options to the hooks:
onCompleted
: It is a callback executed after a successful query/mutation.onError
: Callback executed in case of an occurrence of error.refetchQueries
: It takes an array or function which is used to specify a list of queries that need to be refetched after the mutation is successful.
###Some of the good practices you can follow:###
- Type the data you are sending or receiving during the requests wherever it is required. It enhances readability and understandability.
- As a beginner we often tend to store the data we received from the requests in local states which is not required. Apollo Client provides an in-memory cache in which it stores the data that helps the client to respond to future queries for the same data without sending unnecessary requests. So instead of storing it in local states we can directly access and use it without making repeated requests.
Now I'll be explaining some examples that we have used in our ongoing project, the Litmus Portal.
#LitmusChaos LitmusChaos is an open-source toolset to practice chaos engineering in cloud-native systems. It comes up with a large set of chaos experiments that are hosted on the hub. For further details, you can check out our github repo. Litmus Portal provides a console and UI experience for managing, monitoring, and events around chaos workflows. It is being developed using React and TypeScript for the front-end and Golang for the back-end.
#Examples Without any further delay, let's get started!!
Since the examples I am going to explain are a part of a project, I have excluded some parts of the logic which are not relevant to the blog. You can find the complete code here.
###Query###
Schema
export const GET_USER = gql`
query getUser($username: String!) {
getUser(username: $username) {
username
email
id
name
projects {
members {
user_id
user_name
role
invitation
name
email
joined_at
}
name
id
}
company_name
updated_at
created_at
removed_at
is_email_verified
state
role
}
}
`;
export const ALL_USERS = gql`
query allUsers {
users {
id
name
username
email
}
}
`;
The GET_USER
query string returns the complete details of a user whose username
is passed as a variable.
The ALL_USERS
query string returns a list of all users who are present along with their details including id
, name
, username
, and email
.
useQuery
const { data: dataB } = useQuery<CurrentUserDetails, CurrentUserDedtailsVars>(
GET_USER,
{ variables: { username: userData.username } }
);
const { data: dataA } = useQuery(ALL_USERS, {
skip: !dataB,
onCompleted: () => {
//consoles the list of all users present
console.log(dataA.users);
},
onError: (error) => {
//in case of error, it prints the error message in the console
console.error(error.message)
});
In the above example, I have two queries:
GET_USER
: I am sending theusername
as variable to get all the details associated with that username. The received data can be accessed throughdataB
.CurrentUserDedtailsVars
is the type of data I am sending i.e. the username andCurrentUserDetails
is the type of data I am receiving on a successful query. These types are stored in a separate file:
export interface Member {
user_id: string;
user_name: string;
role: string;
invitation: string;
name: string;
email: string;
joined_at: string;
}
export interface Project {
members: Member[];
name: string;
id: string;
}
export interface UserDetails {
username: string;
projects: Project[];
name: string;
email: string;
id: string;
company_name: string;
updated_at: string;
created_at: string;
removed_at: string;
is_email_verified: string;
state: string;
role: string;
}
export interface CurrentUserDetails {
getUser: UserDetails;
}
export interface CurrentUserDedtailsVars {
username: string;
}
ALL_USERS
: This query is for fetching the list of all the users which can be accessed throughdataA
.
skip
: This is a boolean value, if true, the query will be skipped. In the above logic if dataB
is empty i.e. unless and until GET_USER
query is successful ALL_USERS
query will be skipped. Once dataA
gets populated then the second query is executed. This option is useful in the cases where you need to execute the queries in specific order.
###Mutation###
Schema
export const SEND_INVITE = gql`
mutation sendInvite($member: MemberInput!) {
sendInvitation(member: $member) {
user_id
user_name
role
invitation
}
}
`;
The SEND_INVITE
mutation string is used to send an invitation to a user for a selected project. Once the user accepts the invitation he/she becomes a member of that project too. As MemberInput
we need to send the data which includes the id of the project, the username of the user whom we are going to send the invitation, the role of user in the project name Viewer
or Editor
.
useMutation
// mutation to send invitation to selected users
const [SendInvite, { error: errorB, loading: loadingB }] = useMutation<
MemberInviteNew
>(SEND_INVITE, {
refetchQueries: [{ query: GET_USER, variables: { username } }],
});
In the above mutation, once the invitation is sent(mutation is successful), the GET_USER
query is refetched to update the data.
MemberInviteNew
is the type of data I am sending as variables to mutation string. The interface is defined as follows:
export interface MemberInviteNew {
member: {
project_id: string;
user_name: string;
role: string;
};
}
SendInvite
is the mutate function which can be called whenever you want to execute the mutation.
SendInvite({
variables: {
member: {
project_id: "1234abc",
user_name: "john_doe",
role: "Editor",
},
},
})
#Conclusion#
So these were some of the examples of GraphQL mutation and query. I hope I was able to explain these concepts well, but if you still have some queries or feedback feel free to reach out to me. Since the LitmusChaos project is completely open-source, feel free to contribute in any way possible. Visit the GitHub repo and become one out of the many stargazers.
{% github litmuschaos/litmus %}
Last but not the least, with the upcoming Hacktober Fest, there are many issues for all levels such as good-first issues, front-end issues, complex issues, etc. So even if you are a beginner you can always submit a PR and start contributing to open source. Grab your chance to win a lot of Litmus swags and goodies upon a successful merge. So do not forget to visit the Litmus site and join our community(#litmus channel on the Kubernetes Slack).😇