loading...
Cover image for Comparing hooks libraries for GraphQL

Comparing hooks libraries for GraphQL

bnevilleoneill profile image Brian Neville-O'Neill Originally published at blog.logrocket.com on ・13 min read

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.

LogRocket Free Trial Banner

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.

Chat App Built With GraphQL Hooks

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:

Backend of Chat App

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 with apollo-client. It contains useQuery, useMutation, and useSubscription 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 with apollo-link-http and apollo-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 under apollo-link
  • apollo-utilities provides utility functions for apollo-client
  • apollo-cache-inmemory provides caching functionalities for GraphQL requests
  • subscription-transport-ws is used with apollo-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:

  1. Login
  2. Chat
  3. Chat item

Chat App Components — Apollo

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 as useQuery, useMutation, and useSubscriptions
  • subscriptions-transport-ws -provides SubscriptionClient 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.

Chat App Components — GraphQL

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.

Alt Text

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.

Posted on by:

bnevilleoneill profile

Brian Neville-O'Neill

@bnevilleoneill

Director content @LogRocket. I didn't write the post you just read. To find out who did, click the link directly above my name.

Discussion

pic
Editor guide