Build a Task Manager CRUD App with React and Hygraph

Pieces 🌟 - Feb 17 '23 - - Dev Community

A task manager app.

In this article, we will introduce readers to how to build a fully functional CRUD application using Hygraph as our backend, React as our frontend, and ApolloClient to manage our state and fetch and mutate our data. We’ll leverage the Content and Mutation APIs that the Hygraph software exposes to us to perform a simple example of querying and mutating data using a task manager app: creating tasks, reading tasks, updating tasks, and finally deleting a task with Apollo Client. We’ll also demonstrate how to set up and manage our content and connect Hygraph to React.

What is CRUD?

CRUD is an abbreviation for Create, Read, Update, and Delete. These are the four basic operations that a software application should be able to perform. These applications allow users to generate data, read data from the UI, update data, and delete data.

CRUD apps consist of three components in fully fledged applications:

  • API (or server): the code and procedures.
  • Database: stores information and makes it accessible to users.
  • User interface (UI): makes it easier for users to use the application.

When using Restful APIs to perform CRUD operations and making API requests, GET, POST, PUT, and DELETE are the most commonly used HTTP methods. GraphQL uses two types of API requests: Queries and Mutations. A query is used to read the data while mutation is used to create, update and delete the data, which we’ll do once we’ve built our task manager.

Why use Hygraph for our Task Manager?

Hygraph, formerly known as GraphCMS, is a backend-only content management system (i.e., a headless CMS) that uses GraphQL to query data and perform mutations (or updates) to the content, making it accessible via a single endpoint (API) for display on any device without a built-in frontend or presentation layer. It allows teams to use a single content repository to deliver content from a single source to endless frontend platforms via API, such as websites, mobile apps, TVs, and so on. Hygraph also allows teams to use any preferred tech stack or framework, including popular ones like React, Angular, or Vue. It integrates easily with Netlify, Vercel, and Gatsby Cloud for quick previews.

To follow this tutorial, you need:

Building the Backend Data Structure in Hygraph

Before you have access to the Hygraph admin panel, you’ll need to create an account (if you don’t have one already). Hygraph is simple and user-friendly, providing you with an intuitive UI and a good user experience.

Once you’ve signed up, create a project name, choose the region where you want your data to be stored and served, and choose a plan. For our task manager project, we are using a free forever plan.

From our admin dashboard, on the left, below environments, click on Schema. This will allow us to create a model. We have named our content type Task and added fields to our content.

The Hygraph Schema page.

Based on the image above, we created three fields:

  • title - (single-line text) - The title of the task.
  • description - (Multi-line text) - The description of the task.
  • assigned to - (Multi-line text) - Who the task is assigned to.

Adding Content

Even though we can create tasks from the front end of our task manager app, we can also create new tasks from your Hygraph Admin Dashboard. Click on Content, then Create entry. Fill out the information.

The content page of the Hygraph dashboard.

Once you have filled out the available fields, click on the save and publish button. You can go back and create more items.

Hygraph API Playground

Hygraph provides us with a GraphQL API playground where we can test our queries and perform mutation queries.

The GraphQL API playground in Hygraph.

You can play around with this Hygraph API environment to see the data you are querying.

Experimenting with data in the API playground.

Setting up Roles and Permissions

Before we begin to query this data inside our React App, we must first get and set up our API endpoint and permissions to open or query any published content.

Go to Project settings > Permanent Auth Tokens > add token, input the name of your token, and click on add & configure permission. Then click on Add permission to add permissions. With this, anybody can make a public API request without requiring authentication.

A screengrab of the permissions window.

Store your token somewhere secure. Later on, we’ll use it to authenticate and fetch data from our React task manager app.

Building our Task Manager Frontend with React

In this section, we will install React and other dependencies that we will use to build our app. In your terminal, run either of the following commands:

#yarn
yarn create react-app project-manager && cd project-manager
Enter fullscreen mode Exit fullscreen mode

Or

#npm
npx create-react-app project-manager && cd project-manager
Enter fullscreen mode Exit fullscreen mode

To make the development of our application easier, we will use Material-UI. This is a React UI library that adheres to Google's Material Design and offers React components right out of the box to develop our UI. In addition to the Material UI, you will need to use some libraries to connect to our backend (Hygraph):

  • graphql - this package provides logic for parsing GraphQL queries. It is used for interpreting GraphQL queries and mutations.
  • apollo-client - this is a tool that helps connect to our Hygraph GraphQL server. It’s also used to fetch, cache, and modify application data, while automatically updating your UI.
  • react-router-dom - a library that makes it possible to integrate dynamic routing into web applications. It enables you to show pages and lets users navigate through them.

Run the command below to install react-router-dom into your React app.

yarn add react-router-dom
Enter fullscreen mode Exit fullscreen mode

After installing react-router-dom, you need to make it available anywhere in your task manager app. To do this, you need to import BrowserRouter from react-router-dom and then wrap the root (App) component in your index.js file.

root.render(
  <BrowserRouter>
      <App />
  </BrowserRouter>
);
Enter fullscreen mode Exit fullscreen mode

To install Material UI, run the command below in your terminal:

yarn add @mui/material @emotion/react @emotion/styled @mui/icons-material
Enter fullscreen mode Exit fullscreen mode

Connecting Hygraph to React using Apollo Client

Install Apollo Client into our project by running the command below in your terminal:

yarn add @apollo/client graphql
Enter fullscreen mode Exit fullscreen mode

To query tasks from our Hygraph endpoint, we need to develop a GraphQL client that will make our query available across our app. This is something we can do right in the index.js file:

import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from "react-router-dom"
import App from './App';

import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
  uri: process.env.REACT_APP_GCMS_API,
  cache: new InMemoryCache(),
  headers: {
      Authorization: `Bearer ${process.env.REACT_APP_GCMS_AUTH}`,
    },
});

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <BrowserRouter>
    <ApolloProvider client={client}>
      <App />
    </ApolloProvider>
  </BrowserRouter>
);
Enter fullscreen mode Exit fullscreen mode

Here we implement the Apollo client and inject it into the application by wrapping it with the Apollo provider.

Create a file in the project’s root directory named .env. Add the following to .env:

REACT_APP_HYG_API= your api key
REACT_APP_HYG_AUTH= your api token
Enter fullscreen mode Exit fullscreen mode

Setting up Routes in our Task Manager Backend

In our src folder, create a new folder called component. In this components folder, we’ll create the following files:

  • Tasks.js - a template component for displaying a single task entry. We will also perform the delete and update operation on the file.
  • TaskList.js - This is a page with a list of all task data.
  • BottomNav.js - for navigating throughout the app.
  • AddTasks.js - a page with a form for adding new tasks.

In your BottomNav.js file, add the code below:

import { AddTask, Task } from '@mui/icons-material';
import { BottomNavigation, BottomNavigationAction } from '@mui/material';
import React, { useState } from 'react'

const BottomNav = () => {
  const [value, setValue] = useState('task');

  return (
    <div>
        <BottomNavigation
            showLabels
            sx={{bgcolor: '#292f38'}}
            value={value}
            onChange={(event, newValue) => {
              setValue(newValue);
            }}
        >
          <BottomNavigationAction href='/' sx={{color: '#ccc'}} label="Tasks" icon={<Task />} />
          <BottomNavigationAction href='/new ' sx={{color: '#ccc'}} label="AddTask" icon={<AddTask />} />
        </BottomNavigation>
  </div>
  )
}
export default BottomNav
Enter fullscreen mode Exit fullscreen mode

We use Material UI to create navigation in our task manager that allows us to navigate through the list of tasks as well as creating a task.

import React from 'react';
import { Route, Routes } from "react-router-dom";
import AddTask from './components/AddTask';
import TaskList from './components/TaskList';
import BottomNav from './components/BottomNav';
import './App.css'

function App() {
  return (
      <div className='container'>
        <div className='app-wrapper'>
          <div className='header'>
            <h1>Task Manager</h1>
          </div>
          <div className='main'>
            <Routes>
              <Route path="/" element={<TaskList />} />
              <Route path="/new" element={<AddTask /> } />
            </Routes>
          </div>
          <BottomNav />
        </div>
      </div>
  );
}
export default App;
Enter fullscreen mode Exit fullscreen mode

Here, we used features from the react-router-dom library to define our routes and their paths and attach them to their respective components.

We have added additional styles to our application. Update your App.css with the code below:

* {
  margin: 0;
  padding: 0;
}
.container {
  background: linear-gradient(100deg, rgb(182, 40, 111) 50%, #ac2066 0);
  width: 100%;
  padding: 20px;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
}
.app-wrapper {
  background-color: #292f38;
  width: 30%;
  min-width: 800px;
  height: 600px;
  padding: 30px;
  box-sizing: border-box;
  border-radius: 5px;
  box-shadow: 0 15px 30px rgba(0, 0, 0, 0.4);
}
.header h1 {
  color: #ccc;
  font-weight: 300;
  text-align: center;
  margin: 50px 20px 60px 20px;
  font-family: 'Josefin Slab', serif;
}
.main {
  display: flex;
  flex-direction: column;
  align-items: space-between;
  margin-bottom: 50px;
  width: 100%;
}
.list {
  width: 90%;
  margin: auto;
  max-height: 300px;
  overflow: hidden;
  overflow-y: auto;
}
/* width */
::-webkit-scrollbar {
  width: 10px;
}
/* Track */
::-webkit-scrollbar-track {
  border-radius: 10px;
  background-color: #aaa;
}
/* Handle */
::-webkit-scrollbar-thumb {
  background: #444;
  border-radius: 10px;
}
.list-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin: 20px 0;
  padding-bottom: 5px;
}
.list-item span {
  color: #999;
}
.list-item h2 {
  color: #999;
}

.no-tasks {
  color: #777;
  text-align: center;
  font-size: 18px;
  margin-top: 20px;
}
Enter fullscreen mode Exit fullscreen mode

Querying Content and Displaying Tasks in our Task Manager

Let’s start by creating a file that we’ll use to store all of our queries and mutation queries. The goal is to migrate all of our code with a simple copy of the file. By doing so, we can manage our API-specific interactions in a single file, edit and update code, and reuse it between files.

In the src folder, create a lib/api.js file and add the following code:

import { gql } from '@apollo/client';

// getting all tasks
export const ALL_TASK = gql`
    query {
        tasks(stage: DRAFT) {
            id
            title
            description
            assignedTo
        }
    }
`
Enter fullscreen mode Exit fullscreen mode

We used gql, a function for passing queries from the Apollo client library that we imported from the Apollo client, to wrap and define the query we want to execute. In our query, we passed a variable DRAFT because we also want to fetch the task in the draft. We are doing this because we won’t perform the published task operation from our front end in this tutorial.

Publishing tasks from the front end without going to the Hygraph dashboard is possible. Read the Hygraph documentation to learn how.

In your components/Tasks.js file, add the following code:

import React  from 'react'
import {AssignmentInd, Delete, Description, Update} from '@mui/icons-material';
import { List, ListItemButton, ListItemIcon, ListItemText, Typography, Stack, Button, Modal, Box, FormControl, OutlinedInput, InputLabel } from '@mui/material';

const Task = ({ task, getTask }) => {

  return (
    <li className='list-item'>
        <List sx={{ width: '100%'}} component="nav" aria-labelledby="nested-list-subheader"
        >
        <Typography sx={{color: '#ccc'}} variant="h5" gutterBottom>
            {task.title}
        </Typography>
        <ListItemButton>
            <ListItemIcon>
             <Description sx={{ color: '#ccc'}} />
            </ListItemIcon>
            <ListItemText primary={task.description} />
        </ListItemButton>
        <ListItemButton>
            <ListItemIcon>
                <AssignmentInd sx={{ color: '#ccc'}} />
            </ListItemIcon>
            <ListItemText primary={task.assignedTo} />
        </ListItemButton>
        </List>
        <Stack direction="row" spacing={1}>
            <Button className='btn-delete task-btn'>
                <Delete
                sx={{bgcolor: '#292f38', color: '#ccc'}}
                />
            </Button>
            <Button className='btn-delete task-btn'>
                <Update
                sx={{bgcolor: '#292f38', color: '#ccc'}}
                />
            </Button>
        </Stack>
    </li>
  )
}
export default Task
Enter fullscreen mode Exit fullscreen mode

We used Material UI to build out our front end where we display the title, description, and assignee of each task. We pass task and getTask as props.

In your component/TaskList.js file, add the following code:

import React from 'react'
import { useQuery } from '@apollo/client';
import { ALL_TASK } from '../lib/api';
import Task from './Task';
import { Typography } from '@mui/material';

const TaskList = () => {
  const { loading, error, data } = useQuery(ALL_TASK)

  if (loading) return <p>Getting tasks...</p>;
  if (error) return <p>An error occurred</p>;

  return (
    <div>
        <Typography sx={{color: '#ccc'}} variant="p" gutterBottom>
            Total Tasks: {data.tasks.length}
        </Typography>
        {data.tasks.length ?
            (
                <ul className='list'>
                    {data.tasks.map((task) => (
                        <Task task={task} key={task.id} getTask={ALL_TASK} />
                    ))}
                </ul>
            )
            :
            (
                <div className='no-tasks'>No Tasks</div>
            )
        }
    </div>
  )
}
export default TaskList;
Enter fullscreen mode Exit fullscreen mode

We imported the useQuery hook provided by the Apollo client and we passed the ALL_TASK GraphQL query to it. We defined three states for the data in our hook.

  • loading - this is helpful while the query is being processed.
  • error - when the query was unsuccessfully processed.
  • data - this contains data returned by Hygrapyh.

Inside the data object, we now have access to the tasks_._ When the application renders the component, the useQuery hook will be called.

The task manager UI window.

Creating New Tasks with Mutation

To create a task in your task manager, in your lib/api.js file, add the query below:

export const CREATE_TASK =  gql`
    mutation CreateTask($assignedTo: String, $description: String, $title: String) {
        createTask(
            data: {assignedTo: $assignedTo, description: $description, title: $title}
          ) {
            id
            title
            description
            assignedTo
        }
    }
`
Enter fullscreen mode Exit fullscreen mode

Before now, we’ve been calling the backend for data using queries. We now want to play around with them a bit, but to do so, we need to employ mutations. To perform the CRUD operations create, update, and delete, we used mutation as an operation type. We export the CREATE_TASK query; when we build a new model, Hygraph automatically provides a custom function called create__ for us. The name of the model to be created is always prefixed to it. The createTask function was necessary because our model was given the name Task. Use createTask as an operation name and pass on our variables that are required by the backend.

In your AddTask.js file, add the code below:

import React, { useState } from 'react'
import { useMutation } from '@apollo/client'
import { CREATE_TASK } from '../lib/api';
import {OutlinedInput, FormControl, InputLabel, Button, Box} from '@mui/material'

const AddTask = () => {
  const [task, setTask] = useState({});
  const [createTask, { isadding }] = useMutation(CREATE_TASK)

  if (isadding) return 'Submitting...';

  const handleOnChange = (event)=> {
    setTask({ ...task, [event.target.name]: event.target.value});
  }
  const onClick = () => {
    createTask({variables: { ...task }});
  }
  return (
    <Box
    sx={{ maxWidth: '100%'}}>
      <FormControl fullWidth sx={{ my: 1 }}>
        <InputLabel sx={{color: '#cccc'}}>Title</InputLabel>
        <OutlinedInput
          onChange={handleOnChange}
          name='title'
          label="title"
          sx={{border: '1px solid #cccc'}}
        />
      </FormControl>
      <FormControl fullWidth sx={{ my: 1 }}>
        <InputLabel sx={{color: '#cccc'}}>Description</InputLabel>
         <OutlinedInput
          onChange={handleOnChange}
          name='description'
          label="description"
          sx={{border: '1px solid #cccc'}}
        />
      </FormControl>
      <FormControl fullWidth sx={{ my: 1 }}>
        <InputLabel sx={{color: '#cccc'}}>Assigned To</InputLabel>
        <OutlinedInput
          onChange={handleOnChange}
          name='assignedTo'
          label="Assigned To"
          sx={{border: '1px solid #cccc'}}
        />
      </FormControl>
       <Button href='/' onClick={onClick} type='submit' sx={{ my: 1, py: 2 }} fullWidth variant="contained">Add Task</Button>
    </Box>
  )
}

export default AddTask
Enter fullscreen mode Exit fullscreen mode

First, we used the useState hook from React to store the provided state. We then used the useMutation hook from the Apollo client. The useMutation hook depends on the createTask function to execute the CREATE_TASK mutation query. If the createTask function gets called, the mutation gets executed. We then use the handleOnChange function to update the state whenever the user inputs data.

After entering dummy content and clicking on Add task in our task manager, if all goes well, a new task will be created and you will be able to view your content in the Hygraph Dashboard. Newly created tasks don’t automatically get published unless we call the publish function or publish it from the dashboard. But, we already defined our query to also fetch content from the draft, so newly created content will automatically be displayed on our front end.

The task creation flow in our task manager app.

Updating Tasks in our Task Manager

It's easy to update posts by simply adding new content to existing content and updating the Hygraph store. In our lib/api.js file, add the query below:

export const UPDATE_TASk = gql`
    mutation UpdateTask($assignedTo: String, $description: String, $title: String, $id: ID){
        updateTask(
            where: {id: $id}
            data: {assignedTo: $assignedTo, description: $description, title: $title}
        ) {
            assignedTo
            description
            title
        }
}
`
Enter fullscreen mode Exit fullscreen mode

Updating entities using mutations is similar to creating new ones, except you need two arguments in your query: the where object referencing the id of the task you want to update and the data object that holds the data to replace the old content.

We'll need a form to collect the data we'll need to update a task. We’ll create a modal so that when a user tries to update a task by clicking on the update icon, it will call up a modal containing the form.

const [open, setOpen] = useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
const [replacementTask, setReplacementTask] = useState({});

const handleOnChange = (event)=> {
    setReplacementTask({ ...replacementTask, [event.target.name]: event.target.value });
}
const [updateTask] = useMutation(UPDATE_TASk, {
    refetchQueries: [
        { query: getTask },
    ]
});
const handleUpdate = () => {
    updateTask({ variables: { id: task.id, ...replacementTask }})
}
Enter fullscreen mode Exit fullscreen mode

To better control the process of creating and updating content, we define several states. We then define functions that are responsible for handling form input and calling the Hygraph API services in our query.

Add the handleOpen function to the Update icon:

<Button className='btn-delete task-btn'>
    <Update
    sx={{bgcolor: '#292f38', color: '#ccc'}}
    onClick={handleOpen}
    />
</Button>
Enter fullscreen mode Exit fullscreen mode

With this, when you click on the update button a modal will pop up containing the form field. Now below the Button container, add the modal component from Material UI to build our form field.

<Modal open={open} onClose={handleClose} aria-labelledby="modal-modal-title"
      aria-describedby="modal-modal-description">
  <Box sx={style}>
    <FormControl fullWidth sx={{ my: 1 }}>
        <InputLabel sx={{color: '#cccc'}}>Title</InputLabel>
        <OutlinedInput
            onChange={handleOnChange}
            name='title'
            label="Title"
            sx={{color: '#cccc'}}
        />
    </FormControl>
    <FormControl fullWidth sx={{ my: 1 }}>
      <InputLabel sx={{color: '#cccc'}}>Description</InputLabel>
        <OutlinedInput
            onChange={handleOnChange}
            name='description'
            label="Description"
            sx={{color: '#cccc'}}
         />
    </FormControl>
    <FormControl fullWidth sx={{ my: 1 }}>
        <InputLabel sx={{color: '#cccc'}}>Assigned To</InputLabel>
        <OutlinedInput
            onChange={handleOnChange}
            name='assignedTo'
            label="Assigned To"
            sx={{color: '#cccc'}}
        />
    </FormControl>
  <Button href='/' sx={{ my: 1, py: 2 }} fullWidth variant="contained"
    onClick={handleUpdate} 
    type='submit'>Update</Button>
  </Box>
</Modal>
Enter fullscreen mode Exit fullscreen mode

With this, we can now successfully update our task. You will have to republish the task after editing it from the Hygraph dashboard.

Updating the metadata of a task.

Deleting Tasks

In this section, we’ll work on deleting each task from the front end of our task manager. In our lib/api.js file, add the following code:

export const DELETE_TASK = gql`
    mutation DeleteTask($id: ID!) {
        deleteTask(where: {id: $id}) {
            id
            title
            description
            assignedTo
        }
    }
`
Enter fullscreen mode Exit fullscreen mode

Here, we define the DELETE_TASK that we used to delete our task, and referenced the id variable of the task to be deleted.

Update your component/Task.js file with the following code:

const [deleteTask] = useMutation(DELETE_TASK, {
    refetchQueries: [
        { query: getTask },
    ]
})
const handleDelete = () => {
    deleteTask({ variables: { id: task.id }});
}
Enter fullscreen mode Exit fullscreen mode

We also added the property refetchQueries supplied by the useMutation hook to re-fetch our data to reflect the modifications brought on by deleting a task. We called the deleteTask function with the handleDelete function and passed the data of the id to the variable we defined.

<Button className='btn-delete task-btn'>
    <Delete
      sx={{bgcolor: '#292f38', color: '#ccc'}}
      onClick={handleDelete}
    />
</Button>
Enter fullscreen mode Exit fullscreen mode

Deleting a task from our task manager.

Conclusion

In this tutorial, we learned about the Hygraph headless CMS and how to use Hygraph to create a model, manage content, and set up roles and permissions. Using Hygraph, Apollo Client, React Router Dom, Material UI, and React, we were able to develop a fully functional CRUD task manager application.

You can find the source code for this article on GitHub.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .