A Full Stack Guide to Graphql: React Client

Jawakar Durai's avatar

Jawakar Durai

This is the final post on the series of GraphQL posts.

We started with intro to GraphQl, implemented a simple NodeJs GraphQl service to understand the concepts like Resolvers, Schemas (typeDefs),Query and Mutation, and then we built the same GraphQL service in elixir with Absinthe.

In this post, we're going to build the UI using ReactJs and Apollo graphql client for the menu card service we wrote. Let's get started.

Like every React example starts:

$ npx create-react-app menu_card_client
$ cd menu_card_client
$ yarn start

Let us add GraphQL dependencies we need:

$ yarn add @apollo/client graphql
  • @apollo/client is a GraphQL client library to fetch data via GraphQL
  • graphql is a core GraphQL library apollo uses

We're ready to code! 👨‍💻

Edit index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { ApolloProvider, ApolloClient, InMemoryCache } from '@apollo/client';
 
const client = new ApolloClient({
  url: process.env.REACT_APP_API_URL,
  cache: new InMemoryCache(),
});
 
ReactDOM.render(
  <ApolloProvider client={client}>
    <React.StrictMode>
      <App />
    </React.StrictMode>
  </ApolloProvider>,
  document.getElementById('root')
);
 
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

Update your .env to

REACT_APP_API_URL=https://localhost:4000

set REACT_APP_GRAPHQL_URL to the graphql service endpoint you're running.

Lines we added:

const client = new ApolloClient({
  url: process.env.REACT_APP_GRAPHQL_URL,
  cache: new InMemoryCache(),
});

The client is the interface between our React app and the GraphQL service, we are telling it to use the service from the provided url and to use configurable cache.

If you already didn't have the GraphQL node server running, start the service and continue or clone the repo:

$ git clone https://github.com/jawakarD/node-graphql-menu.git && cd node-graphql-menu && yarn && yarn start.
ReactDOM.render(
  <ApolloProvider client={client}>
    <React.StrictMode>
      <App />
    </React.StrictMode>
  </ApolloProvider>,
  document.getElementById('root')
);

For the client to be consumed by the hooks we use in our Application, pass the App component as a child to ApolloProvider which acts as a context provider and passes the client as a context.

That's all in index.js.

Let us move to functionalities:

Don't worry about css, delete the content on App.css, copy and paste this gist.

Get menu items - useQuery

useQuery hook allows us to query the graphql service as we did in the server-side playground, but here we are going to use react to call those queries.

basic usage:

const { loading, error, data } = useQuery(QUERY, options);

useQuery takes two parameters

  1. GraphQL query string, for the query we are going to make
  2. options will have the variables and other options we can pass

Edit your App.js to have two components MenuItem and App:

// App.js
 
const MenuItem = ({ name, id, price, rating }) => {
  //just for fun
  const COLORS = ['#ff00f6', '#00ff50', '#fff900', '#ff8300'];
  const color = useMemo(
    () => COLORS[Math.floor(Math.random() * COLORS.length)],
    []
  );
 
  return (
    <div className="menu-item">
      <div className="info">
        <p className="name" style={{ color }}>
          {`${name} *${rating}`}
        </p>
        <p className="item-price" style={{ color }}>
          {price}
        </p>
      </div>
      <div className="action">
        <button type="button" style={{ color, borderColor: color }}>
          Delete
        </button>
      </div>
    </div>
  );
};
 
const GET_MENU_ITEMS = gql`
  query {
    menuItems {
      id
      name
      price
      rating
    }
  }
`;
 
const App = () => {
  const { loading, error, data } = useQuery(GET_MENU_ITEMS);
  if (loading) {
    return <p>Loading...</p>;
  }
  if (error) {
    return <p>error</p>;
  }
 
  const { menuItems } = data;
 
  return (
    <div className="App">
      <div className="App-body">
        <div className="add-form">
          <button>Add Item</button>
        </div>
        <div className="menu-box">
          {menuItems.map((item) => (
            <MenuItem key={item.id} {...item} />
          ))}
        </div>
      </div>
    </div>
  );
};

In the App component, we call useQuery hook with GET_MENU_ITEMS query to fetch the list of menu items.

The hook returns different states of the data that is being fetched: loading, error (if any in the query, actual data for the query, and other values.

The query will also be cached. If same is called again in another component, apollo will fetch the data from the cache that will make the subsequent queries much faster. Another interesting thing is, you can also interact with the stored cache. But it's not recommended to use apollo cache as a state management like redux, because redux will give us more control over updating data than apollo cache.

useQuery also allows other features: Polling (execute query periodically at a specified interval) and Refetching (Refetch the query). You might want to opt useLazyQuery hook for cases where the actual execution of the graphql query needs to happen on some other user action or event, as opposed to the default behaviour of execution. Read more on this here.

Now, if you turn into the browser, you can see the list of currently available menus. If it's empty, you can wait to add some when we go to the useMutation section.

Create menu item - useMutation

useMutation allows us to run mutations with the graphql server.

basic usage:

const [mutateFunction, { called, loading, data, error }] = useMutation(
  MUTATION,
  options
);

Like useQuery, useMutation takes two arguments: gql query string and mutation options. But like useLazyQuery it doesn't do any API call on its own but returns a tuple containig:

  1. a mutate function is to execute mutation at any time
  2. an object with state of the mutation: called, loading, data and error

To add a new menu item using the UI, add a Form component, and incorporate that in the App component.

Edit your App.js to have Form component and use that in the App component:

 
const CREATE_MENU_ITEM = gql`
  mutation addMenuItem($name: String!, $price: Int!, $rating: Int) {
    addMenuItem(params: { name: $name, price: $price, rating: $rating }) {
      id
      name
      price
      rating
    }
  }
`;
 
const Form = () => {
  const [name, setName] = useState("");
  const [price, setPrice] = useState();
  const [rating, setRating] = useState();
 
  const [addMenuItem] = useMutation(CREATE_MENU_ITEM, {
    update(cache, { data: { addMenuItem } }) {
      const { menuItems } = cache.readQuery({ query: GET_MENU_ITEMS });
      cache.writeQuery({
        query: GET_MENU_ITEMS,
        data: { menuItems: [addMenuItem, ...menuItems] },
      });
    },
  });
 
  const onSubmit = (e) => {
    e.preventDefault();
 
    addMenuItem({
      variables: {
        name,
        price: Number(price),
        rating: Number(rating),
      },
    });
  };
 
  return (
    <>
      <form className="form">
        <div className="form-group">
          <label htmlFor="name">Name</label>
          <input
            type="text"
            onChange={(e) => setName(e.target.value)}
            id="name"
            value={name}
          />
        </div>
        <div className="form-group">
          <label htmlFor="price">Price</label>
          <input
            type="number"
            onChange={(e) => setPrice(e.target.value)}
            id="price"
            value={price}
          />
        </div>
        <div className="form-group">
          <label htmlFor="rating">Rating</label>
          <input
            type="number"
            onChange={(e) => setRating(e.target.value)}
            id="rating"
            value={rating}
          />
        </div>
      </form>
      <button type="button" onClick={onSubmit}>
        Submit
      </button>
    </>
  );
};
 
// Edit App component to:
const App = () => {
>  const [formOpen, setFormOpen] = useState(false);
  const { loading, error, data } = useQuery(GET_MENU_ITEMS);
 
  if (loading) {
    return <p>Loading...</p>;
  }
  if (error) {
    return <p>error</p>;
  }
 
  const { menuItems } = data;
 
  return (
    <div className="App">
      <div className="App-body">
        <div className="add-form">
>          <button onClick={() => setFormOpen((o) => !o)}>Add Item</button>
>          {formOpen && <Form />}
        </div>
        <div className="menu-box">
          {menuItems.map((item) => (
            <MenuItem key={item.id} {...item} />
          ))}
        </div>
      </div>
    </div>
  );
};

update function:

const [addMenuItem] = useMutation(CREATE_MENU_ITEM, {
  update(cache, { data: { addMenuItem } }) {
    const { menuItems } = cache.readQuery({ query: GET_MENU_ITEMS });
    cache.writeQuery({
      query: GET_MENU_ITEMS,
      data: { menuItems: [addMenuItem, ...menuItems] },
    });
  },
});

Here we are passing options to the useMutation, But we can also pass options to the mutate function when we're calling the addMutation function.

The options we pass to the mutate function will override the options in useMutation.

update function is a callback, that will be called after the a successful mutation, with arguments data that mutation returns and the current cache interface.

Through mutation, you're only updating the server-side data.

If a mutation updates a single existing entity, apollo-graphql will automatically update that entity's value in its cache when the mutation returns with the updated entity's id.

Apart from updating, if we are doing other updates(addition and deletion) using mutation, we have to update the cache to reflect the changes manually.

So, understanding update function we are using in above useMutation:

  • Get the cache of the query GET_MENU_ITEMS:
const { menuItems } = cache.readQuery({ query: GET_MENU_ITEMS });
  • cache.writeQuery used to write data to a specific query, here are updating data of GET_MENU_ITEMS query with the newly created data using spread operator:
cache.writeQuery({
  query: GET_MENU_ITEMS,
  data: { menuItems: [addMenuItem, ...menuItems] },
});

On submit:

const onSubmit = (e) => {
  e.preventDefault();
 
  addMenuItem({
    variables: {
      name,
      price: Number(price),
      rating: Number(rating),
    },
  });
};

Here, we are passing variables in the options that will be used in the mutation mutation addMenuItem($name: String!, $price: Int!, $rating: Int) to create a menu item.

Save the file and go to the browser to add a menu item. After you submit the form, you will see the submitted menu item will be added to the list because of the update function.

You can take an exercise to implement deleting a menu item and updating the cache for the delete function.

query and mutation are the main things to know in graphql. There's also a topic called subscriptions, which uses WebSocket to push changes to the client instead of us querying or mutating.

Github Repo.

Thanks.