Laravelにlighthouseを導入して、ReactでApollo Clientを使って簡単なCRUDアプリを作る!!!
この記事では以下のような簡単CRUDアプリを作っていきたいと思います!
バックエンド Laravel,lighthouse
Laravelにlighthouseを導入することにより、LaravelをGraphQLサーバーとして扱うことができるようになります。
実際にGraphQLを利用したCRUDアプリを作成していきたいと思います。
Laravelプロジェクトを作成します。色々選択肢があります。お好きなのをどうぞ。
モデルとマイグレーションファイルを作成する
php artisan make:model Tweet --migration
マイグレーションファイル作成とシーディングを行う
このパートはメインではなく準備になるのでコードは抜粋しています。
tweetsテーブルのスキーマを定義します。超シンプルは構成です。
public function up()
{
Schema::create('tweets', function (Blueprint $table) {
$table->id();
$table->string('content');
$table->timestamps();
});
}
マイグレートを行います。
php artisan migrate
テーブルが完成したのでダミーデーターを作成していきます。factoryとseederを利用します。
public function definition()
{
return [
'content' => $this->faker->text,
];
}
シーダーを記述してシーディングを行います。
public function run()
{
// \App\Models\User::factory(10)->create();
Tweet::factory(100)->create();
}
php artisan db:seed
これでダミーデータを100件作成することができました。
laravelにlighthouseを導入する
プロジェクトディレクトリにてコンポーザー経由でインストールを行います。
composer require nuwave/lighthouse
そして以下のartisanコマンドを入力すると、lighthouseが用意したデフォルトのスキーマを発行することができます。
php artisan vendor:publish --tag=lighthouse-schema
するとgraphqlディレクトリの中に、schema.graphqlファイルが作成されます。
内容としてはUser型と2つのQueryが定義されています。今回はTweetのデータだけを扱う予定なので触りません。
GraphQL開発には欠かせないツールをインストールする
以下のコードでGraphQL Playgroundをインストールすることができます。
mysqlをphpmyadminで操作する人が多いですが、そのgraphqlバージョンと考えていいと思います。
自分はフロントからバックエンドにクエリを投げる前にクエリ文が合っているのかプレイグラウンドで投げて
確かめています。
composer require mll-lab/laravel-graphql-playground
実際にGraphQLクエリを投げて値を取得する
schema.graphqlを以下のようにします。
phpを書かずにモデルの操作を行うことができました。lighthouseすごいですね。
文字通り@allで全てのTweetを取得しています。この@はlighthouseのディレクティブと呼ばれる機能で、
例えばlaravelのModel::all()とやっていたのを@allのように記述することができます。
用意されているディレクティブの一覧
scalar DateTime
@scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTime")
type Query {
#ツイートを全て取得
tweets: [Tweet!]! @all
}
type Tweet {
id: ID!
content: String!
}
上記にCreate、UpDate、Deleteを追加して以下のようになりました。
CRUD機能をこんなに簡単に作ることができました。
scalar DateTime
@scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTime")
type Query {
#ツイートを全て取得
tweets: [Tweet!]! @all
}
type Mutation {
#ツイート作成
createTweet(content: String!): Tweet! @create
#ツイートアップデート
updateTweet(id: ID!, content: String!): Tweet! @update
#ツイート削除
deleteTweet(id: ID!): Tweet! @delete
}
type Tweet {
id: ID!
content: String!
}
上のコードの簡単な補足です。
- !は、そのフィールドがnullにならないことを表している。
- ): Tweetは返り値を表している。type Tweetの構造のデータを取得することができる。
- Queryはバックエンドから固定の結果を処理で、Readで使われます。ex)一覧取得、条件による検索など
- DBのデータに変更を加えるCreate、UpDate、DeleteはMutation
https://lighthouse-php.com/5/the-basics/schema.html#types
フロントエンド React,ApolloClient
Reactプロジェクトの作成
まず以下のコマンドでReactとTypeScriptの環境を作ります。
npx create-react-app graphql-crud-frontend --template typescript
Apollo Clientの導入
https://www.apollographql.com/docs/react/get-started/ を参考に行います。
以下のコマンドを実行して、Reactのデフォルトのページが表示されれば成功です。
npm start
npm install @apollo/client graphql
Apollo Clientの初期化
uriにはapiサーバーのエンドポイントを設定します。rest形式のapiだと情報を取得するための様々なエンドポイントが存在しますが、graphqlでは1つのエンドポイントで様々なクエリを投げることができます。。
const client = new ApolloClient({
uri: "http://127.0.0.1:8000/graphql",
cache: new InMemoryCache(),
});
root.render(
<React.StrictMode>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</React.StrictMode>
);
corsの問題が発生
作業をしていく中でcorsの問題が発生すると思うのでgraphqlサーバー側でレスポンスヘッダーをいじる必要があります。以下の記事が非常に参考になります。
全ての通信で作成するミドルウェアが挟まるように
Reactからクエリを投げて、情報を取得する
では試しにクエリを投げていきたいと思います。クエリを投げるにはuseQueryを使用します。
useQueryの返り値であるオブジェクトから分割代入法によりloadingとerror、dataを取得します。
loadingがtrueの場合はデータを取得している最中なので、その場合は「Tweetを取得中です...」というメッセージを画面に表示するようにしています。メッセージもありですが、スピナーを表示すると一気に様になります。
import { useQuery } from "@apollo/client";
import { GET_ALL_TWEETS } from "./gql/crud";
interface DATA {
__typename: string;
id: string;
content: string;
}
interface ARRAY_DATA {
tweets: Array<DATA>;
}
function App() {
const { loading, error, data } = useQuery<ARRAY_DATA>(GET_ALL_TWEETS);
if (loading) return <>Tweet取得中です...</>;
if (error) return <>Tweet取得に失敗しました...</>;
console.log(data);
return (
<div className="App">
{data?.tweets.map((tweet) => (
<p key={tweet.id}>
{tweet.id}:{tweet.content}
</p>
))}
</div>
);
}
export default App;
useQueryの第一引数であるGET_ALL_TWEETSは別ファイルに定義したクエリ文のことで以下のように定義をしています。クエリを直接useQueryの引数に書くことはできますが、クエリだけを記述するファイルを作成し、定義することが多いです。
import { gql } from "@apollo/client";
export const GET_ALL_TWEETS = gql`
query GetAllTweets {
tweets {
id
content
}
}
`;
また今回はqueryを発行し、かつapollo clientの初期化の際にcache: new InMemoryCache()を設定したので、取得したデータがキャッシュとして保存されています。キャッシュとして保存しておくことにより、
ページアクセスするたびに、サーバーに問い合わせるのではなく、キャッシュの有無によってサーバーに問い合わせを行うのかを決めることが可能になります。
キャッシュを可視化するChromeの拡張機能があります。
キャッシュの情報を取得するには__typenameとidを組み合わせたものをkeyとして取得することが可能になります。
create
ミューテーションの定義
まずミューテーションを以下のように定義します。$inputは変数で、入力した文字列を変数に入れ、ミューテーションを投げます。変数を使う場合はCreateTweetの部分は省略できません。変数使う使わない関係なく書く場合が多いです。
export const CREATE_TWEET = gql`
mutation CreateTweet($input: String!) {
createTweet(content: $input) {
id
content
}
}
`;
カスタムhooksの作成
useMutationの返り値のcreateTweetは実際にミューテーションを投げる関数です。このフックスではcreateTweet関数だけ返すようにしています。
import { useMutation } from "@apollo/client";
import React from "react";
import { CREATE_TWEET } from "../gql/crud";
export const useCreate = () => {
const [createTweet, { data, loading, error }] = useMutation(CREATE_TWEET, {
onCompleted(completed) {
console.log("成功しました");
},
onError(error) {
console.log(error.graphQLErrors);
},
});
return { createTweet };
};
ミューテーションを投げる
Tweet!ボタンが押された時に、フックスで取得したcreateTweet関数を呼び出しています。
refetch関数を使うことにより再度サーバーにクエリを投げています。こうすることでサーバー側とキャッシュ側でのデータの差異を防ぐことができます。refetchするのではなく、既存のキャッシュを操作する方法もあります。
import { useQuery } from "@apollo/client";
import React from "react";
import { GET_ALL_TWEETS } from "./gql/crud";
import { useCreate } from "./hook/useCreate";
interface DATA {
__typename: string;
id: string;
content: string;
}
interface ARRAY_DATA {
tweets: Array<DATA>;
}
function App() {
const { loading, error, data, refetch } =
useQuery<ARRAY_DATA>(GET_ALL_TWEETS);
const { createTweet } = useCreate();
if (loading) return <>Tweet取得中です...</>;
if (error) return <>Tweet取得に失敗しました...</>;
//Create
const clickCreateButton = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const tweet = document.querySelector("#tweet") as HTMLInputElement;
createTweet({ variables: { input: tweet.value } });
refetch();
tweet.value = "";
};
return (
<div className="App">
<form name="submitTweetForm" onSubmit={(e) => clickCreateButton(e)}>
<input type="text" id="tweet" />
<button>Tweet!</button>
</form>
{data?.tweets.map((tweet) => (
<p key={tweet.id}>
{tweet.id}:{tweet.content}
</p>
))}
</div>
);
}
export default App;
update
ミューテーションの定義
export const UPDATE_TWEET = gql`
mutation UpdateTweet($id: ID!, $content: String!) {
updateTweet(id: $id, content: $content) {
id
content
}
}
`;
カスタムhooksの作成
import { useMutation } from "@apollo/client";
import React from "react";
import { UPDATE_TWEET } from "../gql/crud";
export const useUpdate = () => {
const [updateTweet, { data, loading, error }] = useMutation(UPDATE_TWEET, {
onCompleted(completed) {
console.log("成功しました");
},
onError(error) {
console.log(error.graphQLErrors);
},
});
return { updateTweet };
};
ミューテーションを投げる
ポイントはCREATEの時のようにrefetchを行なっていない点です。既存の単一のアイテムを更新するミューテーションは自動でキャッシュを更新してくれます。
import { useQuery } from "@apollo/client";
import React from "react";
import { GET_ALL_TWEETS } from "./gql/crud";
import { useCreate } from "./hook/useCreate";
import { useUpdate } from "./hook/useUpdate";
interface DATA {
__typename: string;
id: string;
content: string;
}
interface ARRAY_DATA {
tweets: Array<DATA>;
}
function App() {
const { loading, error, data, refetch } =
useQuery<ARRAY_DATA>(GET_ALL_TWEETS);
const { createTweet } = useCreate();
const { updateTweet } = useUpdate();
if (loading) return <>Tweet取得中です...</>;
if (error) return <>Tweet取得に失敗しました...</>;
//Create
const clickCreateButton = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const tweet = document.querySelector("#tweet") as HTMLInputElement;
createTweet({ variables: { input: tweet.value } });
refetch();
tweet.value = "";
};
//Update
const clickUpdateButton = (
e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
id: string
) => {
const tweet = document.querySelector(`#tweet${id}`) as HTMLInputElement;
updateTweet({ variables: { id, content: tweet.value } });
};
return (
<div className="App">
<form name="submitTweetForm" onSubmit={(e) => clickCreateButton(e)}>
<input type="text" id="tweet" />
<button>Tweet!</button>
</form>
{data?.tweets.map((tweet) => (
<div key={tweet.id}>
<p>{tweet.id}</p>
<textarea id={`tweet${tweet.id}`}>{tweet.content}</textarea>
<button onClick={(e) => clickUpdateButton(e, tweet.id)}>
update!!!
</button>
</div>
))}
</div>
);
}
export default App;
delete
ミューテーションの定義
export const DELETE_TWEET = gql`
mutation DeleteTweet($id: ID!) {
deleteTweet(id: $id) {
id
content
}
}
`;
カスタムhooksの作成
import { useMutation } from "@apollo/client";
import React from "react";
import { DELETE_TWEET } from "../gql/crud";
export const useDelete = () => {
const [deleteTweet, { data, loading, error }] = useMutation(DELETE_TWEET, {
onCompleted(completed) {
console.log("成功しました");
},
onError(error) {
console.log(error.graphQLErrors);
},
});
return { deleteTweet };
};
ミューテーションを投げる
import { useQuery } from "@apollo/client";
import React from "react";
import { GET_ALL_TWEETS } from "./gql/crud";
import { useCreate } from "./hook/useCreate";
import { useUpdate } from "./hook/useUpdate";
import { useDelete } from "./hook/useDelete";
interface DATA {
__typename: string;
id: string;
content: string;
}
interface ARRAY_DATA {
tweets: Array<DATA>;
}
function App() {
const { loading, error, data, refetch } =
useQuery<ARRAY_DATA>(GET_ALL_TWEETS);
const { createTweet } = useCreate();
const { updateTweet } = useUpdate();
const { deleteTweet } = useDelete();
if (loading) return <>Tweet取得中です...</>;
if (error) return <>Tweet取得に失敗しました...</>;
//Create
const clickCreateButton = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const tweet = document.querySelector("#tweet") as HTMLInputElement;
createTweet({ variables: { input: tweet.value } });
refetch();
tweet.value = "";
};
//Update
const clickUpdateButton = (id: string) => {
const tweet = document.querySelector(`#tweet${id}`) as HTMLInputElement;
updateTweet({ variables: { id, content: tweet.value } });
};
//Delete
const clickDeleteButton = (id: string) => {
deleteTweet({ variables: { id } });
refetch();
};
return (
<div className="App">
<form name="submitTweetForm" onSubmit={(e) => clickCreateButton(e)}>
<input type="text" id="tweet" />
<button>Tweet!</button>
</form>
{data?.tweets.map((tweet) => (
<div key={tweet.id}>
<p>{tweet.id}</p>
<textarea id={`tweet${tweet.id}`}>{tweet.content}</textarea>
<br />
<button onClick={() => clickUpdateButton(tweet.id)}>update!!!</button>
<button onClick={() => clickDeleteButton(tweet.id)}>delete!!!</button>
</div>
))}
</div>
);
}
export default App;
これで一応完成となります!
さいごに
次回はMaterial UIを使っていい感じに見栄えを良くしていきたいと思います。自分自身フロントエンドやGraphQLに関して初心者なので、コメントいただけたらありがたいです。
Discussion
LGTM