React、Prisma、GraphQLを使用してレシピアプリを構築する方法

提供:Dev Guides
移動先:案内検索

序章

GraphQL は、 REST API に比べてさまざまな利点があるため、フロントエンド開発の点で人気を博しました。 ただし、独自の GraphQL サーバーのセットアップは、エラーが発生しやすく、複雑です。 このため、 Prisma などのマネージドサービスがGraphQLサーバーを管理するようになり、アプリの開発に集中できるようになりました。

このチュートリアルでは、ReactPrismaを使用してGraphQLを管理する完全に機能するレシピアプリを構築します。

前提条件

  • JavascriptとReactの中間知識
  • GraphQLの基礎
  • Dockerの基礎

ステップ1—依存関係のインストール

次のコマンドを実行して、PrismaCLIクライアントをグローバルにインストールします。

npm install -g prisma

create-react-appを使用してReactアプリをブートストラップするので、次のコマンドを実行してグローバルにインストールします。

npm install -g create-react-app

Prismaをローカルで使用するには、マシンにDockerがインストールされている必要があります。 Dockerをまだお持ちでない場合は、 Docker CommunityEditionをダウンロードできます。

ステップ2—Prismaの設定

Prisma CLIを使用するには、Prismaアカウントが必要です。 Prisma Webサイトでアカウントを作成し、次のコマンドを実行してPrismaCLIにログインできます。

prisma login

必要な依存関係がすべて揃ったので、プロジェクト用のフォルダーを作成し、次のコマンドを実行してフォルダーに移動します。

mkdir recipe-app-prisma-react 
cd recipe-app-prisma-react

次に、Prismaサーバーをフォルダーで初期化します。

prisma init

プロンプトが表示され、prismaサーバーのセットアップに使用する方法に関するいくつかのオプションが示されます。 今のところサーバーをローカルで操作し、後でデプロイします。 Create new databaseを選択して、PrismaにDockerを使用してローカルにデータベースを作成させます。

次に、データベースを選択するためのプロンプトが表示されます。 このチュートリアルではPostgresを使用するため、PostgreSQLを選択します。

次に、生成されたプリズムクライアントのプログラミング言語を選択する必要があります。 Prisma Javascript Clientを選択します。

選択したオプションに基づいて、Prismaによって次のファイルが生成されます。

ステップ3—Prismaの展開

Prismaサーバーがセットアップされたので、dockerが実行されていることを確認します。 次に、次のコマンドを実行してサーバーを起動します。

docker-compose up -d

Docker compose は、複数のコンテナーを単一のサービスとして実行するために使用されます。 前のコマンドは、PrismaサーバーとPostgresデータベースを起動します。 ブラウザの127.0.0.1:4466にアクセスして、Prismaプレイグラウンドを表示します。

サーバーを停止する場合は、docker-compose stopを実行します。

次に、datamodel.prismaファイルを開き、デモコンテンツを次のように置き換えます。

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

次に、次のコマンドを実行してデモサーバーにデプロイします。

prisma deploy

作成されたモデルとPrismaエンドポイントを示す応答が次のように表示されます。

デプロイされたサーバーを表示するには、https://app.prisma.io/でPrismaダッシュボードを開き、サービスに移動します。 ダッシュボードに次のように表示されます。

ローカルサーバーにデプロイするには、prisma.ymlファイルを開き、エンドポイントをhttp://localhost:4466に変更してから、prisma deployを実行します。

ステップ4—Reactアプリのセットアップ

Prismaサーバーの準備ができたので、PrismaGraphQLエンドポイントを使用するようにReactアプリを設定できます。

プロジェクトフォルダーで、次のコマンドを実行して、create-react-appを使用してクライアントアプリをブートストラップします。

create-react-app client

GraphQLを使用するには、いくつかの依存関係が必要です。 クライアントフォルダに移動し、次のコマンドを実行してインストールします。

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

UIには、 AntDesignを使用します。

npm install antd --save

フォルダ構造:

アプリのフォルダ構造は次のようになります。

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

ステップ5—コードを書く

Index.js

ここでは、apollo構成を行います。 これは、アプリのメインエントリファイルになります。

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

すべてのレシピを取得するためのクエリ:

import { gql } from 'apollo-boost';

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

GetSingleRecipe.js

レシピ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

新しいレシピを作成するための突然変異:

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

レシピを更新するための突然変異:

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

これは、CRUD操作のロジックのベースになっています。 ファイルは非常に大きいため、重要な部分のみを含めました。 残りのコードはGitHubで表示できます。

クエリとミューテーションを使用するには、それらをインポートしてから、react-apollo's graphql を使用する必要があります。これにより、クエリを実行してリアクティブに更新できるhigher-order componentを作成できます。アプリにあるデータに基づいています。

公開されているすべてのレシピを取得して表示する方法の例を次に示します。

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

結果のビューは次のようになります。

注:ファイルサイズのため、コンポーネントのスタイリングは含まれません。 このコードは、GitHubリポジトリで入手できます。


コンポーネントには複数のエンハンサーが必要なため、composeを使用して、コンポーネントに必要なすべてのエンハンサーを組み込みます。

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

また、ApolloClientインスタンスへの直接アクセスを提供するwithApolloエンハンサーも必要です。 レシピのデータをフェッチするために1回限りのクエリを実行する必要があるため、これは便利です。

レシピの作成

次のフォームからデータをキャプチャした後:

次に、次のhandleSubmitコールバックを実行します。これにより、addNewRecipeMutationミューテーションが実行されます。

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

レシピの編集

レシピを編集するために、新しいレシピの作成に使用したフォームを再利用して、レシピデータを渡します。 ユーザーが編集アイコンをクリックすると、次のようにデータが事前に入力されたフォームがポップアップ表示されます。

次に、別のhandleSubmitハンドラーを実行して、次のように更新ミューテーションを実行します。

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

レシピを削除する

削除機能については、削除されたレシピに対してsoft-deleteを実行します。つまり、記事をフェッチするときにフィルタリングするため、published属性をfalseに変更します。 publishedの記事を入手してください。

次の例に示すように、以前と同じ関数を使用し、publicedをfalseとして渡します。

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

結論:

このチュートリアルでは、Prismaを使用してGraphQLサーバーを管理し、ReactとGraphQLを使用してレシピアプリを構築しました。 Prismaは、ビジネスロジックの実装に集中できる信頼性の高いサービスです。

このコードには、GitHubからアクセスできます。