We're live-coding on Twitch! Join us!
Building a RECIPE app using Prisma and React

Building a RECIPE app using Prisma and React

Code Demo

In the last couple of years, GraphQL has taken on the scene in terms of frontend development due to the various advatanges it offers over REST.

However, setting up your own GraphQL server is challenging, it's both error-prone and complicated. This has been made easier due to managed services such as Prisma which does the heavy lifting in your GraphQL server making it easier for you to instead focus on the development of you app.

In this tutorial, we will be building a fully functional Recipe app using Prisma and React.

Prerequisites

  • Intermediate knowledge of Javascript and React
  • GraphQL fundamentals
  • Docker fundamentals - don't panic if you have no prior experience, just copy paste the commands and hopefully all goes well :)

Installation

We need to install the prisma cli client globally by running the following command:

npm install -g prisma

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

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 here.

Prisma Setup

To use the prisma cli, you will need to have a prisma account, you can create an account here then login to prisma cli by running the following command:

prisma login

Now that we have all the required dependecies to kickstart our project installed, head over to your terminal and create a folder for the project (somewhere in your code stash) and navigate into the folder by running the following commands:

Essential Reading: Learn React from Scratch! (2019 Edition)
mkdir recipe-app-prisma-react 
cd recipe-app-prisma-react

Then we initialize our prisma server in the folder:

prisma init

A prompt will appear with a few options on which method you want to use to setup 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

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

Next we have to choose a language for out generated prisma client, choose Prisma Javascript Client(sorry flow and typescript fanatics :( won't be using any type checkers so as to keep the tutorial simple )

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

Deployment

Now that we have our prisma server setup, make sure docker is running and the run the following command to start the server:

docker-compose up -d

Docker compose is used to run multiple containers as a single service, the above command will start our prisma server and the postgres database, head over to 127.0.0.1:4466 to view the prisma playground. Incase you want to kill your server just run docker-compose stop

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:

prisma deploy

You should get a response showing the created models and your prisma endpoint as follows:

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

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

React App Setup

Now that our prisma server is ready, we can setup our react app to consume the prisma GraphQL endpoint.

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

create-react-app client

To work with graphql, we will require a few dependecies, navigate into the client folder and run the following command to install them:

cd client
npm install apollo-boost react-apollo graphql-tag graphql --save

For the UI we will be using Ant Design:

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

The Code

Index.js

In here is where we do the apollo config, it's 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 big, I've omitted the irrelavant parts to make space for the crucial bits, you can view the rest of the code here.

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:

NB: Styling for the components will not be included due to file size, the code is however available in the repo

Since we have require a more than one enhancer in our component, we will use compose to be able 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:

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:

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 basically 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 above and pass in published as false, as shown in the example below:

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'
    });
  };
};

You can access the code here and you can also try out the demo app here.

Conclusion:

Prisma is a very highly reliable service that makes life easier for you as a developer, you get to focus on implementing you business logic and let prisma do the heavy lifting for your GraphQL server.

Like this article? Follow @brayoh_k on Twitter