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.
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.
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.
You can play around with this Hygraph API environment to see the data you are querying.
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.
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
Or
#npm
npx create-react-app project-manager && cd project-manager
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
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>
);
To install Material UI, run the command below in your terminal:
yarn add @mui/material @emotion/react @emotion/styled @mui/icons-material
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
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>
);
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
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
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;
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;
}
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
}
}
`
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
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;
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.
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
}
}
`
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
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.
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
}
}
`
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 }})
}
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>
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>
With this, we can now successfully update our task. You will have to republish the task after editing it from the Hygraph dashboard.
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
}
}
`
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 }});
}
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>
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.