DEV Community

danielpdev
danielpdev

Posted on

Frontend part with React and ApolloGraphQL for a basic TodoApp

Backend is here

Here is the live version on codesandbox

Table of Contents

What is GraphQL?

Simple, a query language used to define an API which provides a complete and understandable description of the data and enables powerful developer tools.
More on GraphQL.

Intro

For our frontend we will use React with ApolloClient for fetching data.
Not all files will be covered in this post because most of them does not contain any graphql related stuff, but you can check them by accessing the live version on codesandbox.

Install prerequisites

Navigate to your projects directory and copy paste the following commands:

mkdir todo-react-graphql && cd todo-react-graphql
npm init react-app todo-react-apollo-app && cd todo-react-apollo-app && npm install apollo-boost apollo-cache-inmemory graphql 

Remove boilerplate code

rm src/*

Code

Entry point (index.js)

Create a file called index.js in src/ and paste the following code

import "./styles.scss";

import { InMemoryCache } from "apollo-cache-inmemory";

import React from "react";
import ReactDOM from "react-dom";

import ApolloClient from "apollo-boost";
import { ApolloProvider } from "react-apollo";
import App from "./components/App";
const URI = "https://apollo-graphql-todo.glitch.me/graphql"; //replace with your own graphql URI

const cache = new InMemoryCache({
  dataIdFromObject: object => object.id || null
});

const client = new ApolloClient({
  cache,
  uri: URI
});

const Root = () => {
  return (
    <ApolloProvider client={client}>
      <App />
    </ApolloProvider>
  );
};

ReactDOM.render(<Root />, document.querySelector("#root"));

We are creating an InMemoryCache instance and we are passing it as cache to our apollo client. InMemoryCache is the default cache implementation for ApolloClient 2.0.
More on apollo caching.
Because we are using MongoDB we can take advantage of the globally unique identifiers that any document is assigned with and stored at _id. This will be our key for every object stored in cache.

const cache = new InMemoryCache({
  dataIdFromObject: object => object.id || null
});

Here we are actually setting the URI to our backend

const client = new ApolloClient({
  cache,
  uri: URI 
});

In render function we are returning our App component wrapped inside an ApolloProvider component and passing our client instance as prop.

const Root = () => {
  return (
    <ApolloProvider client={client}>
      <App />
    </ApolloProvider>
  );
};

Queries

mkdir queries && cd queries && touch index.js
import gql from "graphql-tag";

const ADD_TODO = gql`
  mutation AddTodo($content: String!) {
    addTodo(content: $content) {
      id
      content
    }
  }
`;

const GET_TODOS = gql`
  {
    todos {
      id
      content
      likes
    }
  }
`;

const DELETE_TODO = gql`
  mutation DeleteTodo($ID: ID) {
    deleteTodo(id: $ID) {
      id
    }
  }
`;

const GET_TODO = gql`
  query Todo($id: ID!) {
    todo(id: $id) {
      id
      content
      likes
    }
  }
`;

const TODO_LIKE = gql`
  mutation TodoLike($id: ID) {
    likeTodo(id: $id) {
      id
      likes
    }
  }
`;

const UPDATE_TODO = gql`
  mutation UpdateTodo($id: ID!, $content: String!) {
    updateTodo(id: $id, content: $content) {
      id
    }
  }
`;

export { TODO_LIKE, GET_TODO, DELETE_TODO, GET_TODOS, ADD_TODO, UPDATE_TODO };

Here we are using graphql-tag package to define our queries and mutations. Graphql-tag is used to generate a syntax tree object that we are further using for our queries and mutations. Is a lot similar to what we've written when we tested our graphql backend.
The differences consists in the fact that any query and mutation has to be wrapped around with a keyword name and just like a function you have to specify a signature.

const GET_TODO = gql`
  query Todo($id: ID!) {
    todo(id: $id) {
      id
      content
      likes
    }
  }
`;

We are saying that our query named Todo receives a parameter named id (prefixed with the $ meaning that we are starting to define
a param) which will then be used in our query block and it must be of type ID, ! says that this parameter is non-nullable. ID is a scalar type that represents a unique identifier and usually used as key for a cache.
The same rules are also applied for mutations.
Wrapped around this you can find the actual query that will be run against our backend.
Just play with it and do a console.log(GET_TODO) to see the actual query generated by gql.

Components

cd .. && mkdir components && cd components

TodoList.js

As most of the code from our components it's just basic react, I won't go throughout all of it and I will cover only the parts where graphql is present.
The first component that we will take a look at is TodoList, this is the primary component and it's main responsibility is to load the list of
of todos and display it.

import React from "react";

import Loading from "./Loading";
import TodoItem from "./TodoItem";
import { Query } from "react-apollo";
import { Link } from "react-router-dom";
import { GET_TODOS } from "../queries";

const TodoList = props => (
  <Query query={GET_TODOS}>
    {({ loading, error, data }) => {
      if (loading) return <Loading />;
      if (error) return `Error! ${error.message}`;
      const { todos } = data;

      return (
        <React.Fragment>
          <div className="cards">
            {todos &&
              todos.map(todo => (
                <TodoItem
                  key={todo.id}
                  {...todo}
                  onUpdate={id => {
                    props.history.push(`/todo/${id}`);
                  }}
                />
              ))}
          </div>
          <Link to="/todos/new">
            <i
              className="fas fa-plus-circle fa-2x has-text-success"
              style={{
                float: "right"
              }}
            />
          </Link>
        </React.Fragment>
      );
    }}
  </Query>
);

export default TodoList;

At first, all of the content that is dependent on the response coming from the query must be placed as a child inside a Query component.
One of the props that it receives is the actual query that need to be run against the backend and in our case the query is:

{
    todos {
      id
      content
      likes
    }
}

{({ loading, error, data }) are the props that we are using after our the fetching has finished. This component gets created two times. First when our query is started and second after the query succeeds or fails. In case of a problem with the network or any other errors we will have the error prop defined and containing the error message.
In case of successful our data prop will contain the actual todos converted to js object and ready to be used.

TodoCreate.js

import { Mutation } from "react-apollo";
import React, { useState } from "react";
import useLoading from "./../hooks/useLoading";
import { ADD_TODO, GET_TODOS } from "../queries";

const TodoCreate = props => {
  const [setLoadingButton, setLoadingState] = useLoading({
    classList: ["is-loading", "no-border"]
  });
  const [content, setContent] = useState("");

  return (
    <Mutation
      mutation={ADD_TODO}
      update={(cache, { data: { addTodo } }) => {
        try {
          const { todos } = cache.readQuery({ query: GET_TODOS });
          cache.writeQuery({
            query: GET_TODOS,
            data: { todos: todos.concat([{ ...addTodo, likes: 0 }]) }
          });
        } catch (e) {
        } finally {
          setLoadingState(false);
        }
      }}
    >
      {addTodo => (
        <div className="todo_form">
          <h4 className="todo_form__title">Add Todo</h4>
          <form
            onSubmit={e => {
              e.preventDefault();
              setLoadingState(true);
              addTodo({ variables: { content } });
              setContent("");
            }}
          >
            <div className="field">
              <div className="control">
                <input
                  autoCorrect="false"
                  autoCapitalize="false"
                  className="input"
                  type="text"
                  onChange={e => setContent(e.target.value)}
                />
              </div>
            </div>
            <button
              className="button is-light"
              type="submit"
              ref={setLoadingButton}
            >
              Create Todo
            </button>
          </form>
        </div>
      )}
    </Mutation>
  );
};

export default TodoCreate;

Here we have a mutation component that takes a mutation prop object ADD_TODO which we had defined earlier.

The child of this mutation will receive as the first parameter the actual function that is used to trigger the mutation request.

addTodo({ variables: { content } });

When it's the time to make our request we have to pass an object with a variables property which will then be used to trigger the
request.

     <Mutation
      mutation={ADD_TODO}
      update={...}
      />

After our mutation finishes we get our callback called and the new data will be ready for us.

update={(cache, { data: { addTodo } }) => {
    try {
      const { todos } = cache.readQuery({ query: GET_TODOS });
      cache.writeQuery({
        query: GET_TODOS,
        data: { todos: todos.concat([{ ...addTodo, likes: 0 }]) }
      });
    } catch (e) {
    } finally {
      setLoadingState(false);
    }
  }
}

Because we are using a cache system, we have to mutate the cache by using writeQuery function, passing an object containing our query for which we are writing the cache and the data object with the new contents.

However we also have a prop called refetchQueries that is present on the mutation component, but using this prop will trigger a new request to the backend and this is not desired for anyone because we are going to consume more network resources.

For Update action everything it's the same, make request and update cache.

More on mutations.

TodoLike.js

For the like button we want to simulate the request as being a lot more faster because we don't want to actually wait until the request reach the backend and return if it's successful or failed. This is done by using Optimistic UI, which will let us simulate the results of a mutation and update the UI even before we are receiving a response from the server. In case that our mutation fails, we don't have to do nothing because Apollo will handle the dirty work for us.
How does it looks like?

likeTodo({
  variables: { id },
  optimisticResponse: {
    __typename: "Mutation",
    likeTodo: {
      id,
      __typename: "Todo",
      likes: likes + 1
    }
  }
});

That's it, now when we click on like button our update function that handles the response it's immediately called and it's acting like it's instant. If it fails then the action will be reverted.

As an exercise you can also implement Optimistic UI for our TodoCreate component.

Conclusion

ApolloGraphQL it's a great tool for developing frontends that need to support GraphQL. It can easily speed up development process and provide great modern tooling that help you monitor and analyse your API.

I hope you have enjoyed this article.

Top comments (1)

Collapse
 
moatazabdalmageed profile image
Moataz Mohammady

Thank you for awesome tutorial