React、Prisma、GraphQLを使用してレシピアプリを構築する方法
序章
GraphQL は、 REST API に比べてさまざまな利点があるため、フロントエンド開発の点で人気を博しました。 ただし、独自の GraphQL サーバーのセットアップは、エラーが発生しやすく、複雑です。 このため、 Prisma などのマネージドサービスがGraphQLサーバーを管理するようになり、アプリの開発に集中できるようになりました。
このチュートリアルでは、ReactとPrismaを使用して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からアクセスできます。