Written by Ganesh Mani✏️
React Hooks are stateful functions that are used to maintain the state in a functional component. Basically, they break down complex React components by splitting them into smaller functional blocks.
The main problem with React class components is the need to maintain lots of abstractions, such as higher-order components (HOCs) and render props. React Hooks maintain the logic as a function, eliminating the need to encapsulate it.
Take a look at the following example.
GraphQL is a data query language that fetches only the data it needs rather than fetching all the data from the API. It has two operations: queries and mutations. For real-time data, GraphQL uses a concept called subscriptions.
There are two major React Books libraries: graphql-hooks and apollo/react-hooks. To help you determine which library is best for your next GraphQL project, let’s compare them, examine their features, and weigh the pros and cons.
Project scenario
We’ll spin up a quick project to facilitate our comparison. Let’s implement a chat application that enables the user to log in and send group messages.
Backend setup
I won’t spend too much time on the backend, but here’s a quick glimpse at how I set it up for this application:
Basically, I used Hasura to set up GraphQL and a Postgres database. This easy-to-use tool enables you to create a backend in minutes.
Out backend contains two tables:
- User, which includes information about the users
- Message, which stores all the users’ messages
The backend URL is https://hasura-infiite-loader.herokuapp.com/v1alpha1/graphql; the WebSocket URL is ws://hasura-infiite-loader.herokuapp.com/v1alpha1/graphql.
Apollo and React Hooks
To implement our app using Apollo, React Hooks, and React, we must first set up a React project using the following command.
npx create-react-app apollo-react-hook-example
After that, install all the dependencies of the @apollo/react-hooks
package.
npm install @apollo/react-hooks apollo-client apollo-link-http apollo-link-ws apollo-link apollo-utilities apollo-cache-inmemory subscriptions-transport-ws
That’s a lot of packages! Let’s break them down one by one.
-
@apollo/react-hooks
provides all the React Hooks required to use GraphQL withapollo-client
. It containsuseQuery
,useMutation
, anduseSubscription
to execute all the GraphQL operations -
apollo-client
provides all the packages you need to run the caching operations on the client side. It is often used withapollo-link-http
andapollo-cache-memory
-
apollo-link-http
is a chainable unit of operation that you can apply to your GraphQL request. It executes the unit one after another. Here we use an HTTP link to execute the GraphQL HTTP request -
apollo-link-ws
creates a WebSocket link for the GraphQL client -
apollo-link
the two functionalities described above fall underapollo-link
-
apollo-utilities
provides utility functions forapollo-client
-
apollo-cache-inmemory
provides caching functionalities for GraphQL requests -
subscription-transport-ws
is used withapollo-link-ws
to facilitate GraphQL subscriptions
Now it’s time to set up @apollo/react-hooks
with our application. Import all the packages into App.js
.
import ApolloClient from "apollo-client";
import { ApolloProvider } from "@apollo/react-hooks";
import { WebSocketLink } from "apollo-link-ws";
import { HttpLink } from "apollo-link-http";
import { split } from "apollo-link";
import { getMainDefinition } from "apollo-utilities";
import { InMemoryCache } from "apollo-cache-inmemory";
Set up the HTTP and WebSocket links with the server.
const httpLink = new HttpLink({
uri: "https://hasura-infiite-loader.herokuapp.com/v1alpha1/graphql" // use https for secure endpoint
});
// Create a WebSocket link:
const wsLink = new WebSocketLink({
uri: "ws://hasura-infiite-loader.herokuapp.com/v1alpha1/graphql", // use wss for a secure endpoint
options: {
reconnect: true
}
});
Once we have httpLink
and wsLink
, we need to split the request links so we can send different data to each link.
// using the ability to split links, you can send data to each link
// depending on what kind of operation is being sent
const link = split(
// split based on operation type
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return kind === "OperationDefinition" && operation === "subscription";
},
wsLink,
httpLink
);
Let’s create the Apollo client and configure it to Apollo Provider
// Instantiate client
const client = new ApolloClient({
link,
cache: new InMemoryCache()
});
function App() {
return (
<ApolloProvider client={client}>
<ThemeProvider theme={customTheme}>
<div className="App">
<Routes />
</div>
</ThemeProvider>
</ApolloProvider>
);
}
Complete the source code for App.js
.
import React from "react";
import logo from "./logo.svg";
import "./App.css";
import customTheme from "./theme";
import { ThemeProvider } from "@chakra-ui/core";
import Routes from "./routes";
import ApolloClient from "apollo-client";
import { ApolloProvider } from "@apollo/react-hooks";
import { WebSocketLink } from "apollo-link-ws";
import { HttpLink } from "apollo-link-http";
import { split } from "apollo-link";
import { getMainDefinition } from "apollo-utilities";
import { InMemoryCache } from "apollo-cache-inmemory";
const httpLink = new HttpLink({
uri: "https://hasura-infiite-loader.herokuapp.com/v1alpha1/graphql" // use https for secure endpoint
});
// Create a WebSocket link:
const wsLink = new WebSocketLink({
uri: "ws://hasura-infiite-loader.herokuapp.com/v1alpha1/graphql", // use wss for a secure endpoint
options: {
reconnect: true
}
});
// using the ability to split links, you can send data to each link
// depending on what kind of operation is being sent
const link = split(
// split based on operation type
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return kind === "OperationDefinition" && operation === "subscription";
},
wsLink,
httpLink
);
// Instantiate client
const client = new ApolloClient({
link,
cache: new InMemoryCache()
});
function App() {
return (
<ApolloProvider client={client}>
<ThemeProvider theme={customTheme}>
<div className="App">
<Routes />
</div>
</ThemeProvider>
</ApolloProvider>
);
}
export default App;
Now we’ll create Routes.js
for our application.
import React from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import LoginComponent from "./components/login";
import Chat from "./components/Chat";
const Routes = () => (
<Router>
<Route exact path="/" component={LoginComponent} />
<Route path="/chat" component={Chat} />
</Router>
);
export default Routes;
We have three main components:
- Login
- Chat
- Chat item
Let’s examine these in more detail.
Login component
Functionality for the login component is pretty simple. Our app will have a form where for the user to enter a name and password.
The GraphQL operation we need here is mutation. We’ll use a React Hook called useMutation
. We’ll also use react-hook-form
for form validation and chakraUI
for UI.
import { useMutation } from "@apollo/react-hooks";
import gql from "graphql-tag";
const LOGIN_USER = gql`
mutation InsertUsers($name: String!, $password: String!) {
insert_users(objects: { name: $name, password: $password }) {
returning {
id
name
}
}
}
`;
We have a mutation GraphQL operation that takes name
and password
as parameters and executes the insert_users
mutation.
Next, define the useMutation
hooks inside the login component with mutation GraphQL.
const [insert_users, { data }] = useMutation(LOGIN_USER);
Here is the complete source code for Login
/index.js
:
import React, { useState, useEffect } from "react";
import { useForm } from "react-hook-form";
import {
FormErrorMessage,
FormLabel,
FormControl,
Input,
Button,
Box
} from "@chakra-ui/core";
import { useMutation } from "@apollo/react-hooks";
import gql from "graphql-tag";
const LOGIN_USER = gql`
mutation InsertUsers($name: String!, $password: String!) {
insert_users(objects: { name: $name, password: $password }) {
returning {
id
name
}
}
}
`;
const Login = ({ history }) => {
const [state, setState] = useState({
name: "",
password: ""
});
const [insert_users, { data }] = useMutation(LOGIN_USER);
useEffect(() => {
const user = data && data.insert_users.returning[0];
if (user) {
localStorage.setItem("user", JSON.stringify(user));
history.push("/chat");
}
}, [data]);
const { handleSubmit, errors, register, formState } = useForm();
function validateName(value) {
let error;
if (!value) {
error = "Name is required";
}
return error || true;
}
function validatePassword(value) {
let error;
if (value.length <= 4) {
error = "Password should be 6 digit long";
}
return error || true;
}
const onInputChange = e => {
setState({ ...state, [e.target.name]: e.target.value });
};
const onSubmit = () => {
insert_users({ variables: { name: state.name, password: state.password } });
setState({ name: "", password: "" });
};
return (
<Box>
<form onSubmit={handleSubmit(onSubmit)}>
<FormControl isInvalid={errors.name}>
<FormLabel htmlFor="name">Name</FormLabel>
<Input
name="name"
placeholder="name"
onChange={onInputChange}
ref={register({ validate: validateName })}
/>
<FormErrorMessage>
{errors.name && errors.name.message}
</FormErrorMessage>
</FormControl>
<FormControl isInvalid={errors.password}>
<FormLabel htmlFor="name">Password</FormLabel>
<Input
name="password"
type="password"
placeholder="password"
onChange={onInputChange}
ref={register({ validate: validatePassword })}
/>
<FormErrorMessage>
{errors.password && errors.password.message}
</FormErrorMessage>
</FormControl>
<Button
mt={4}
variantColor="teal"
isLoading={formState.isSubmitting}
type="submit"
>
Submit
</Button>
</form>
</Box>
);
};
export default Login;
Chat component
The chat component will primarily use two GraphQL operations: mutation and subscription. Since our chat app is a real-time application, we need to subscribe to get the updated data.
For that, we need the useSubscription
React Hook to subscribe and the useMutation
Hook to make the HTTP POST request on GraphQL.
import { useMutation, useSubscription } from "@apollo/react-hooks";
import gql from "graphql-tag";
const MESSAGES_SUBSCRIPTION = gql`
subscription {
messages {
id
text
users {
id
name
}
}
}
`;
const SUBMIT_MESSAGES = gql`
mutation InsertMessages($text: String!, $userid: Int!) {
insert_messages(objects: { text: $text, created_user: $userid }) {
returning {
text
created_user
users {
name
id
}
id
}
}
}
`;
MESSAGES_SUBSCRIPTION
is a subscription GraphQL schema definition. SUBMIT_MESSAGES
is a mutation GraphQL schema definition.
We’ll use both in our chat component.
const [insert_messages, { returnData }] = useMutation(SUBMIT_MESSAGES);
const { loading, error, data: { messages } = [] } = useSubscription(
MESSAGES_SUBSCRIPTION
);
Messages from useSubscription
will return updated data whenever there is a change in messages from GraphQL.
Here is the complete source code for Chat
/index.js
:
import React, { useState, useEffect } from "react";
import { Box, Flex, Input } from "@chakra-ui/core";
import ChatItem from "../ChatItem";
import { useMutation, useSubscription } from "@apollo/react-hooks";
import gql from "graphql-tag";
const MESSAGES_SUBSCRIPTION = gql`
subscription {
messages {
id
text
users {
id
name
}
}
}
`;
const SUBMIT_MESSAGES = gql`
mutation InsertMessages($text: String!, $userid: Int!) {
insert_messages(objects: { text: $text, created_user: $userid }) {
returning {
text
created_user
users {
name
id
}
id
}
}
}
`;
const Chat = () => {
const [state, setState] = useState({
text: ""
});
const [insert_messages, { returnData }] = useMutation(SUBMIT_MESSAGES);
const { loading, error, data: { messages } = [] } = useSubscription(
MESSAGES_SUBSCRIPTION
);
const onInputChage = e => {
setState({ [e.target.name]: e.target.value });
};
const onEnter = e => {
if (e.key === "Enter") {
let user = localStorage.getItem("user");
user = JSON.parse(user);
insert_messages({ variables: { text: state.text, userid: user.id } });
setState({ text: "" });
}
};
return (
<Box h="100vh" w="40%" margin="auto">
<Flex direction="column" h="100%">
<Box bg="blue" h="90%" w="100%" border="solid 1px" overflowY="scroll">
{messages &&
messages.map(message => {
return <ChatItem item={message} />;
})}
</Box>
<Box bg="green" h="10%" w="100%">
<Input
placeholder="Enter a message"
name="text"
value={state.text}
onChange={onInputChage}
onKeyDown={onEnter}
size="md"
/>
</Box>
</Flex>
</Box>
);
};
export default Chat;
ChatItem
/index.js
:
import React from "react";
import { Box, Flex, Avatar, Heading, Text } from "@chakra-ui/core";
const ChatItem = ({ item }) => {
return (
<Box h="60px">
<Flex direction="row" alignItems="center" height="100%">
<Avatar size="sm" padding="4px" marginLeft="10px" />
<Flex direction="column" margin="5px">
<Text fontSize="xl" margin="0">
{item.users.name}
</Text>
<Text margin="0">{item.text}</Text>
</Flex>
</Flex>
</Box>
);
};
export default ChatItem;
GraphQL Hooks and React
So far, we’ve shown how to use @apollo/react-hooks
with React. Now let’s walk through how to set up and use graphql-hooks
with a React application.
npm install graphql-hooks subscriptions-transport-ws
-
graphql-hooks
provides hooks for GraphQL operations, such asuseQuery
,useMutation
, anduseSubscriptions
-
subscriptions-transport-ws
-providesSubscriptionClient
for WebSocket to use in GraphQL subscriptions
App.js
:
import React from "react";
import customTheme from "./theme";
import { ThemeProvider } from "@chakra-ui/core";
import { GraphQLClient, ClientContext } from "graphql-hooks";
import { SubscriptionClient } from "subscriptions-transport-ws";
import Routes from "./routes";
import "./App.css";
const client = new GraphQLClient({
url: "https://hasura-infiite-loader.herokuapp.com/v1alpha1/graphql",
subscriptionClient: new SubscriptionClient(
"ws://hasura-infiite-loader.herokuapp.com/v1alpha1/graphql"
)
});
function App() {
return (
<ClientContext.Provider value={client}>
<ThemeProvider theme={customTheme}>
<div className="App">
<Routes />
</div>
</ThemeProvider>
</ClientContext.Provider>
);
}
export default App;
We created a GraphQL client with HTTP and WebSocket links and used it with Context Provider.
Now that we’ve set up GraphQL Hooks, we can use it in our components. We’ll create the same components we created during the @apollo/react-hooks
setup.
Spoiler alert: there is not much of a change in components.
Login component
This will be similar to the Apollo setup except for two things: we’re going to import graphql-hooks
, and we don’t need graphql-tags
to define the schema.
Otherwise, the steps are the same.
import React, { useState, useEffect } from "react";
import { useForm } from "react-hook-form";
import {
FormErrorMessage,
FormLabel,
FormControl,
Input,
Button,
Box
} from "@chakra-ui/core";
import { useMutation } from "graphql-hooks";
const LOGIN_USER = `
mutation InsertUsers($name: String!, $password: String!) {
insert_users(objects: { name: $name, password: $password }) {
returning {
id
name
}
}
}
`;
const Login = ({ history }) => {
const [state, setState] = useState({
name: "",
password: ""
});
const [insert_users, { data }] = useMutation(LOGIN_USER);
useEffect(() => {
const user = data && data.insert_users.returning[0];
if (user) {
localStorage.setItem("user", JSON.stringify(user));
history.push("/chat");
}
}, [data]);
const { handleSubmit, errors, register, formState } = useForm();
function validateName(value) {
let error;
if (!value) {
error = "Name is required";
}
return error || true;
}
function validatePassword(value) {
let error;
if (value.length <= 4) {
error = "Password should be 6 digit long";
}
return error || true;
}
const onInputChange = e => {
setState({ ...state, [e.target.name]: e.target.value });
};
const onSubmit = () => {
insert_users({ variables: { name: state.name, password: state.password } });
setState({ name: "", password: "" });
};
return (
<Box w="50%" margin="auto">
<form onSubmit={handleSubmit(onSubmit)}>
<FormControl isInvalid={errors.name}>
<FormLabel htmlFor="name">Name</FormLabel>
<Input
name="name"
placeholder="name"
onChange={onInputChange}
ref={register({ validate: validateName })}
/>
<FormErrorMessage>
{errors.name && errors.name.message}
</FormErrorMessage>
</FormControl>
<FormControl isInvalid={errors.password}>
<FormLabel htmlFor="name">Password</FormLabel>
<Input
name="password"
type="password"
placeholder="password"
onChange={onInputChange}
ref={register({ validate: validatePassword })}
/>
<FormErrorMessage>
{errors.password && errors.password.message}
</FormErrorMessage>
</FormControl>
<Button
mt={4}
variantColor="teal"
isLoading={formState.isSubmitting}
type="submit"
>
Submit
</Button>
</form>
</Box>
);
};
export default Login;
Chat component
Chat
/index.js
import React, { useState, useEffect } from "react";
import { Box, Flex, Input } from "@chakra-ui/core";
import ChatItem from "../ChatItem";
import { useMutation, useSubscription } from "graphql-hooks";
const MESSAGES_SUBSCRIPTION = `
subscription {
messages {
id
text
users {
id
name
}
}
}
`;
const SUBMIT_MESSAGES = `
mutation InsertMessages($text: String!, $userid: Int!) {
insert_messages(objects: { text: $text, created_user: $userid }) {
returning {
text
created_user
users {
name
id
}
id
}
}
}
`;
const Chat = () => {
const [state, setState] = useState({
text: "",
data: []
});
const [errors, setErrors] = useState(null);
const [insert_messages, { returnData }] = useMutation(SUBMIT_MESSAGES);
// const { loading, error, data: { messages } = [] } = useSubscription(
// MESSAGES_SUBSCRIPTION
// );
useSubscription({ query: MESSAGES_SUBSCRIPTION }, ({ data, error }) => {
if (errors && errors.length > 0) {
setErrors(errors[0]);
return;
}
setState({ ...state, data: data.messages });
});
const onInputChage = e => {
setState({ ...state, [e.target.name]: e.target.value });
};
const onEnter = e => {
if (e.key === "Enter") {
let user = localStorage.getItem("user");
user = JSON.parse(user);
insert_messages({ variables: { text: state.text, userid: user.id } });
setState({ ...state, text: "" });
}
};
return (
<Box h="100vh" w="40%" margin="auto">
<Flex direction="column" h="100%">
<Box bg="blue" h="90%" w="100%" border="solid 1px" overflowY="scroll">
{state.data.map(message => {
return <ChatItem item={message} />;
})}
</Box>
<Box bg="green" h="10%" w="100%">
<Input
placeholder="Enter a message"
name="text"
value={state.text}
onChange={onInputChage}
onKeyDown={onEnter}
size="md"
/>
</Box>
</Flex>
</Box>
);
};
export default Chat;
ChatItem
/index.js
import React from "react";
import { Box, Flex, Avatar, Heading, Text } from "@chakra-ui/core";
const ChatItem = ({ item }) => {
return (
<Box h="60px">
<Flex direction="row" alignItems="center" height="100%">
<Avatar
size="sm"
name={item.users.name}
padding="4px"
marginLeft="10px"
/>
<Flex direction="column" margin="5px">
<Text fontSize="xl" margin="0">
{item.users.name}
</Text>
<Text margin="0">{item.text}</Text>
</Flex>
</Flex>
</Box>
);
};
export default ChatItem;
Key takeaways
Let’s summarize the difference between graphql-hooks
and apollo-react-hooks
by analyzing some of the main concepts.
GraphQL operations
As far as GraphQL operations such as query, mutation, and subscription, both libraries are similar. They both have the same set of hooks that can be used for GraphQL operations.
Caching
Both Apollo hooks and GraphQL hooks have options for caching.
GraphQL Hooks include graphql-hooks-memcache
.
import { GraphQLClient } from 'graphql-hooks'
import memCache from 'graphql-hooks-memcache'
const client = new GraphQLClient({
url: '/graphql',
cache: memCache()
})
Meanwhile, Apollo Hooks provides apollo-cache-inmemory
.
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import { ApolloClient } from 'apollo-client';
const client = new ApolloClient({
link: new HttpLink(),
cache: new InMemoryCache()
});
Another advantage of Apollo caching is that there are additional options to configure the caching, such as getting the data ID from the object and cache redirect. Apollo also provides options for cache interaction.
Middleware
Since Apollo Provides an Apollo Link, we can control the execution of the GraphQL operation by providing links. Common Apollo link functionalities include retries, live queries, alternative caching layers, and offline support.
Server-side rendering
Both GraphQL Hooks and Apollo provide packages for server-side rendering. In my experience, both work well.
Conclusion
You should now have a basic understanding of the packages for implementing React Hooks for GraphQL. So which one is best for your GraphQL project? There’s no right or wrong answer — it all depends on your app’s unique needs and your personal preference. I tend to gravitate toward graphql-hooks
because it is simple to use and easy to implement, but I’d encourage you to try both and see which you like best.
200's only ✅: Monitor failed and show GraphQL requests in production
While GraphQL has some features for debugging requests and responses, making sure GraphQL reliably serves resources to your production app is where things get tougher. If you’re interested in ensuring network requests to the backend or third party services are successful, try LogRocket.
LogRocket is like a DVR for web apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic GraphQL requests to quickly understand the root cause. In addition, you can track Apollo client state and inspect GraphQL queries' key-value pairs.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
The post Comparing hooks libraries for GraphQL appeared first on LogRocket Blog.
Top comments (0)