Tutorial

How To Build a Recipe App Using React, Prisma, and GraphQL

Published on December 12, 2019
author

Brian Njenga

How To Build a Recipe App Using React, Prisma, and GraphQL

Introduction

GraphQL gained popularity in terms of front-end development due to the various advantages it offers over REST APIs. However, setting up your own GraphQL server is both error-prone and complicated. Due to this, managed services such as Prisma have been made to manage your GraphQL server, allowing you to focus on the development of your app.

In this tutorial, we will be building a fully functional recipe app using React and Prisma to manage GraphQL.

Prerequisites

  • Intermediate knowledge of Javascript and React
  • GraphQL fundamentals
  • Docker fundamentals

Step 1 — Installing Dependencies

Install the Prisma CLI client globally by running the following command:

  1. npm install -g prisma

We will be using create-react-app to bootstrap our React app, so run the following command to install it globally:

  1. npm install -g create-react-app

To use Prisma locally, you need to have Docker installed on your machine. If you don’t have Docker yet, you can download the Docker Community Edition.

Step 2 — Setting Up Prisma

To use the Prisma CLI, you will need to have a Prisma account. You can create an account at the Prisma website, then login to the Prisma CLI by running the following command:

  1. prisma login

Now that we have all the required dependencies, create a folder for the project and navigate into the folder by running the following commands:

  1. mkdir recipe-app-prisma-react
  2. cd recipe-app-prisma-react

Then initialize your Prisma server in the folder:

  1. prisma init

A prompt will appear with a few options on which method you’ll want to use to set up your prisma server. We will be working with the server locally for now and then deploy it later. Choose Create new database to have Prisma create a database locally with Docker.

 output

Next, you’ll get a prompt to choose a database. For this tutorial we will be using Postgres ,so choose PostgreSQL:

Prisma database prompt

Next we have to choose a programming language for our generated prisma client. Choose Prisma Javascript Client:

Prisma language prompt

You will have the following files generated by Prisma based on the selected options:

Prisma settings summary

Step 3 — Deploying Prisma

Now that we have our Prisma server set up, make sure docker is running. Then, run the following command to start the server:

  1. docker-compose up -d

Docker compose is used to run multiple containers as a single service. The previous command will start our Prisma server and the Postgres database. Head over to 127.0.0.1:4466 in your browser to view the Prisma playground.

If you want to stop your server, run docker-compose stop.

Next, open your datamodel.prisma file and replace the demo content with the following:

type Recipe {
  id: ID! @unique
  createdAt: DateTime!
  updatedAt: DateTime!
  title: String! @unique
  ingredients: String!
  directions: String!
  published: Boolean! @default(value: "false")
}

Then run the following command to deploy to a demo server:

  1. prisma deploy

You will get a response showing the created models and your Prisma endpoint as follows:

Output showing the created models and Prisma endpoint

To view the deployed server, open you Prisma dashboard at https://app.prisma.io/ and navigate to services. You will have the following showing in your dashboard:

Deployed server in Prisma dashboard

To deploy to your local server, open the prisma.yml file and change the endpoint to http://localhost:4466, then run prisma deploy

Step 4 — Setting Up the React App

Now that our Prisma server is ready, we can set up our React app to consume thePrisma GraphQL endpoint.

In the project folder, run the following command to bootstrap our client app using create-react-app:

  1. create-react-app client

To work with GraphQL, we require a few dependencies. Navigate into the client folder and run the following commands to install them:

  1. cd client
  2. npm install apollo-boost react-apollo graphql-tag graphql --save

For the UI, we will be using Ant Design:

  1. npm install antd --save

Folder Structure:

Our app folder structure will be as follows:

src
├── components
│   ├── App.js
│   ├── App.test.js
│   ├── RecipeCard
│   │   ├── RecipeCard.js
│   │   └── index.js
│   └── modals
│       ├── AddRecipeModal.js
│       └── ViewRecipeModal.js
├── containers
│   └── AllRecipesContainer
│       ├── AllRecipesContainer.js
│       └── index.js
├── graphql
│   ├── mutations
│   │   ├── AddNewRecipe.js
│   │   └── UpdateRecipe.js
│   └── queries
│       ├── GetAllPublishedRecipes.js
│       └── GetSingleRecipe.js
├── index.js
├── serviceWorker.js
└── styles
    └── index.css

Step 5 — Writing the Code

Index.js

Here we will do the apollo config. This will be the main entry file for our app:

import React from 'react';
import ReactDOM from 'react-dom';
import ApolloClient from 'apollo-boost';
import { ApolloProvider } from 'react-apollo';

import App from './components/App';

// Pass your prisma endpoint to uri
const client = new ApolloClient({
  uri: 'https://eu1.prisma.sh/XXXXXX'
});

ReactDOM.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>,
  document.getElementById('root')
);

GetAllPublishedRecipes.js

Query to fetch all recipes:

import { gql } from 'apollo-boost';

export default gql`query GetAllPublishedRecipes {
    recipes(where: { published: true }) {
      id
      createdAt
      title
      ingredients
      directions
      published
    }
  }`;

GetSingleRecipe.js

Query to fetch a recipe by the recipe id:

import { gql } from 'apollo-boost';

export default gql`query GetSingleRecipe($recipeId: ID!) {
    recipe(where: { id: $recipeId }) {
      id
      createdAt
      title
      directions
      ingredients
      published
    }
  }`;

AddNewRecipe.js

The mutation for creating a new recipe:

import { gql } from 'apollo-boost';

export default gql`mutation AddRecipe(
    $directions: String!
    $title: String!
    $ingredients: String!
    $published: Boolean
  ) {
    createRecipe(
      data: {
        directions: $directions
        title: $title
        ingredients: $ingredients
        published: $published
      }
    ) {
      id
    }
  }`;

UpdateRecipe.js

The mutation for updating a recipe:

import { gql } from 'apollo-boost';

export default gql`mutation UpdateRecipe(
    $id: ID!
    $directions: String!
    $title: String!
    $ingredients: String!
    $published: Boolean
  ) {
    updateRecipe(
      where: { id: $id }
      data: {
        directions: $directions
        title: $title
        ingredients: $ingredients
        published: $published
      }
    ) {
      id
    }
  }`;

AllRecipesContainer.js

This is where our logic for the CRUD operations is based. The file is quite large, so we’ve included only the crucial parts. You can view the rest of the code on GitHub.

In order to use our queries and mutations, we need to import them and then use the react-apollo's graphql that allows us to create a higher-order component that can execute queries and update reactively based on the data we have in our app.

Here is an example of how we can fetch and display all published recipes:

import React, { Component } from 'react';
import { graphql } from 'react-apollo';

import { Card, Col, Row, Empty, Spin } from 'antd';

// queries
import GetAllPublishedRecipes from '../../graphql/queries/GetAllPublishedRecipes';

class AllRecipesContainer extends Component {
  render() {
    const { loading, recipes } = this.props.data;

    return (
      <div>
        {loading ? (
          <div className="spin-container">
            <Spin />
          </div>
        ) : recipes.length > 0 ? (
          <Row gutter={16}>
            {recipes.map(recipe => (
              <Col span={6} key={recipe.id}>
                <RecipeCard
                  title={recipe.title}
                  content={
                    <Fragment>
                      <Card
                        type="inner"
                        title="Ingredients"
                        style={{ marginBottom: '15px' }}
                      >
                        {`${recipe.ingredients.substring(0, 50)}.....`}
                      </Card>
                      <Card type="inner" title="Directions">
                        {`${recipe.directions.substring(0, 50)}.....`}
                      </Card>
                    </Fragment>
                  }
                  handleOnClick={this._handleOnClick}
                  handleOnEdit={this._handleOnEdit}
                  handleOnDelete={this._handleOnDelete}
                  {...recipe}
                />
              </Col>
            ))}
          </Row>
        ) : (
          <Empty />
        )}
      </div>
    );
  }
}

graphql(GetAllPublishedRecipes)(AllRecipesContainer);

The resulting view would look as follows:

Displayed recipes view in test app

Note: Styling for the components will not be included due to file size. The code is available in the GitHub repo.

Since we require more than one enhancer in our component, we will use compose to incorporate all needed enhancers for the component:

import React, { Component } from 'react';
import { graphql, compose, withApollo } from 'react-apollo';

// queries
import GetAllPublishedRecipes from '../../graphql/queries/GetAllPublishedRecipes';
import GetSingleRecipe from '../../graphql/queries/GetSingleRecipe';

// mutations
import UpdateRecipe from '../../graphql/mutations/UpdateRecipe';
import AddNewRecipe from '../../graphql/mutations/AddNewRecipe';

// other imports

class GetAllPublishedRecipes extends Component {
    // class logic
}

export default compose(
  graphql(UpdateRecipe, { name: 'updateRecipeMutation' }),
  graphql(AddNewRecipe, { name: 'addNewRecipeMutation' }),
  graphql(GetAllPublishedRecipes)
)(withApollo(AllRecipesContainer));

We also require the withApollo enhancer, which provides direct access to your ApolloClient instance. This will be useful, since we need to carry out one-off queries for fetching data for a recipe.

Creating a recipe

After capturing the data from the following form:

Data form for new recipes in test app

We then execute the following handleSubmit callback, which runs the addNewRecipeMutation mutation:

class GetAllPublishedRecipes extends Component {
  //other logic
   _handleSubmit = event => {
    this.props
      .addNewRecipeMutation({
        variables: {
          directions,
          title,
          ingredients,
          published
        },
        refetchQueries: [
          {
            query: GetAllPublishedRecipes
          }
        ]
      })
      .then(res => {
        if (res.data.createRecipe.id) {
          this.setState(
            (prevState, nextProps) => ({
              addModalOpen: false
            }),
            () =>
              this.setState(
                (prevState, nextProps) => ({
                  notification: {
                    notificationOpen: true,
                    type: 'success',
                    message: `recipe ${title} added successfully`,
                    title: 'Success'
                  }
                }),
                () => this._handleResetState()
              )
          );
        }
      })
      .catch(e => {
        this.setState((prevState, nextProps) => ({
          notification: {
            ...prevState.notification,
            notificationOpen: true,
            type: 'error',
            message: e.message,
            title: 'Error Occured'
          }
        }));
      });
  };
};

Editing a recipe

In order to edit a recipe, we re-use the form used to create a new recipe and then pass the recipe data. When a user clicks on the edit icon, the form pops up with the data pre-filled as follows:

Pre-filled editing form for recipe test app

We then run a different handleSubmit handler to run the update mutation as follows:

class GetAllPublishedRecipes extends Component {
  // other logic
  _updateRecipe = ({
    id,
    directions,
    ingredients,
    title,
    published,
    action
  }) => {
    this.props
      .updateRecipeMutation({
        variables: {
          id,
          directions,
          title,
          ingredients,
          published: false
        },
        refetchQueries: [
          {
            query: GetAllPublishedRecipes
          }
        ]
      })
      .then(res => {
        if (res.data.updateRecipe.id) {
          this.setState(
            (prevState, nextProps) => ({
              isEditing: false
            }),
            () =>
              this.setState(
                (prevState, nextProps) => ({
                  notification: {
                    notificationOpen: true,
                    type: 'success',
                    message: `recipe ${title} ${action} successfully`,
                    title: 'Success'
                  }
                }),
                () => this._handleResetState()
              )
          );
        }
      })
      .catch(e => {
        this.setState((prevState, nextProps) => ({
          notification: {
            ...prevState.notification,
            notificationOpen: true,
            type: 'error',
            message: e.message,
            title: 'Error Occured'
          }
        }));
      });
  };
}

Deleting a recipe

As for the delete functionality, we will be doing a soft-delete on the deleted recipe, which means we will be changing the published attribute to false since when fetching the articles we filter to make sure we only get published articles.

We will use the same function as before and pass in published as false, as shown in the following example:

class GetAllPublishedRecipes extends Component {
   // other logic 
   _handleOnDelete = ({ id, directions, ingredients, title }) => {
    // user confirmed delete prompt 
    this._updateRecipe({
      id,
      directions,
      ingredients,
      title,
      published: false, // soft delete the recipe
      action: 'deleted'
    });
  };
};

Conclusion:

In this tutorial, you built a recipe app with React and GraphQL, using Prisma to manage your GraphQL server. Prisma is a reliable service that allows you to focus on implementing you business logic.

You can access the code at GitHub.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the authors
Default avatar
Brian Njenga

author

While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Become a contributor for community

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

DigitalOcean Documentation

Full documentation for every DigitalOcean product.

Resources for startups and SMBs

The Wave has everything you need to know about building a business, from raising funding to marketing your product.

Get our newsletter

Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.

New accounts only. By submitting your email you agree to our Privacy Policy

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Get started for free

Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

*This promotional offer applies to new accounts only.