KeihakuOh / GraphQL_React_App

0 stars 0 forks source link

GraphQLについて #7

Open KeihakuOh opened 2 months ago

KeihakuOh commented 2 months ago

Query Fragments(クエリフラグメント) は、GraphQLのクエリ内で再利用可能な部分

fragment UserFields on User {
  id
  firstName
  age
}

query {
  user(id: "123") {
    ...UserFields
  }
}
KeihakuOh commented 2 months ago
const mutation = new GraphQLObjectType({
  name: 'Mutation',
  fields: {
    addUser: {
      type: UserType,
      args: {
        firstName: { type: new GraphQLNonNull(GraphQLString) },
        age: { type: GraphQLInt },
        companyId: { type: GraphQLString }
      },
      resolve(parentValue, { firstName, age, companyId }) {
        return axios.post('http://localhost:3000/users', { firstName, age, companyId })
          .then(res => res.data);
      }
    }
  }
});

GraphQLNonNullはnullだと許せないって意味

type: UserType, // ここでミューテーションの戻り値がUserTypeであることを指定

KeihakuOh commented 2 months ago

nameを定義する必要性:

{
  user(id: "123") {
    firstName
    age
    company {
      name
    }
  }
}

このクエリで使われている user フィールドの型は UserType です。UserTypeの name: 'User' がスキーマ内で指定されていることで、GraphQLはこのクエリが User 型に基づいたデータを要求していると理解します。

KeihakuOh commented 2 months ago

GraphQLクライアントとしてのライブラリ(Lokka vs Apollo vs Relay)

Lokka

Lokkaは、シンプルで軽量なGraphQLクライアントです。比較的古いライブラリであり、状態管理やキャッシングの機能は提供していませんが、シンプルさが魅力

const { Lokka } = require('lokka');
const { Transport } = require('lokka-transport-http');

// GraphQLエンドポイントを指定してクライアントを作成
const client = new Lokka({
  transport: new Transport('https://your-graphql-endpoint.com/graphql')
});

// クエリを送信し、データを取得
client.query(`
  {
    user(id: "1") {
      name
      email
    }
  }
`).then(result => {
  console.log(result);
}).catch(error => {
  console.error(error);
});

Apollo

特徴 状態管理(Apollo Clientを使ったローカル状態の管理) 高度なキャッシング リアルタイムのサブスクリプションサポート リアクティブなUIと親和性が高い(Reactなどとの統合がスムーズ)

import { ApolloClient, InMemoryCache, gql, ApolloProvider, useQuery } from '@apollo/client';

// Apollo Clientの設定
const client = new ApolloClient({
  uri: 'https://your-graphql-endpoint.com/graphql',
  cache: new InMemoryCache(),
});

// クエリを定義
const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      name
      email
    }
  }
`;

// コンポーネントでクエリを実行
function User({ userId }) {
  const { loading, error, data } = useQuery(GET_USER, {
    variables: { id: userId },
  });

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <div>
      <h1>{data.user.name}</h1>
      <p>{data.user.email}</p>
    </div>
  );
}

// アプリケーション全体にApolloProviderを適用
function App() {
  return (
    <ApolloProvider client={client}>
      <User userId="1" />
    </ApolloProvider>
  );
}

export default App;

Relay

Relayは、Facebookが開発した高度に最適化されたGraphQLクライアントで、大規模なアプリケーションやパフォーマンスが重要な場面でよく使われます。Apolloに比べてやや複雑ですが、効率的なデータ取得、キャッシング、パフォーマンスの最適化が特徴です。

KeihakuOh commented 2 months ago

GraphQLサーバとしてのライブラリ(Apollo vs GraphQL express)

どっちがいいとかはないが、apolloの方は将来変動ありそう

Apollo

// Apollo Serverのインポート
const { ApolloServer, gql } = require('apollo-server');

// サンプルユーザーデータ
const users = [
  { id: '1', name: '太郎', email: 'taro@example.com' },
  { id: '2', name: '花子', email: 'hanako@example.com' },
];

// GraphQLスキーマの定義
const typeDefs = gql`
  type User {
    id: ID!
    name: String!
    email: String!
  }

  type Query {
    user(id: ID!): User
    users: [User]
  }
`;

// リゾルバの定義
const resolvers = {
  Query: {
    // 特定のIDに基づいてユーザー情報を取得
    user: (parent, args) => users.find(user => user.id === args.id),
    // すべてのユーザー情報を取得
    users: () => users,
  },
};

// Apollo Serverを作成
const server = new ApolloServer({ typeDefs, resolvers });

// サーバーの起動
server.listen().then(({ url }) => {
  console.log(`🚀 サーバーが起動しました: ${url}`);
});

GraphQL express

const graphql = require('graphql');
const axios = require('axios');
const {
  GraphQLObjectType,
  GraphQLString,
  GraphQLInt,
  GraphQLSchema,
  GraphQLList,
  GraphQLNonNull,
} = graphql;

const CompanyType = new GraphQLObjectType({
  name: 'Company',
  fields: () => ({
    id: { type: GraphQLString },
    name: { type: GraphQLString },
    description: { type: GraphQLString },
    users: {
      type: new GraphQLList(UserType),
      resolve(parentValue, args) {
        return axios
          .get(`http://localhost:3000/companies/${parentValue.id}/users`)
          .then((res) => res.data);
      },
    },
  }),
});

const UserType = new GraphQLObjectType({
  name: 'User',
  fields: () => ({
    id: { type: GraphQLString },
    firstName: { type: GraphQLString },
    age: { type: GraphQLInt },
    company: {
      type: CompanyType,
      resolve(parentValue, args) {
        return axios
          .get(`http://localhost:3000/companies/${parentValue.companyId}`)
          .then((res) => res.data);
      },
    },
  }),
});

const RootQuery = new GraphQLObjectType({
  name: 'RootQueryType',
  fields: {
    user: {
      type: UserType,
      args: { id: { type: GraphQLString } },
      resolve(parentValue, args) {
        return axios
          .get(`http://localhost:3000/users/${args.id}`)
          .then((resp) => resp.data);
      },
    },
    company: {
      type: CompanyType,
      args: { id: { type: GraphQLString } },
      resolve(parentValue, args) {
        return axios
          .get(`http://localhost:3000/companies/${args.id}`)
          .then((resp) => resp.data);
      },
    },
  },
});

const mutation = new GraphQLObjectType({
  name: 'Mutation',
  fields: {
    addUser: {
      type: UserType,
      args: {
        firstName: { type: new GraphQLNonNull(GraphQLString) },
        age: { type: GraphQLInt },
        companyId: { type: GraphQLString },
      },
      resolve(parentValue, { firstName, age, companyId }) {
        return axios
          .post('http://localhost:3000/users', { firstName, age, companyId })
          .then((res) => res.data);
      },
    },
    deleteUser: {
      type: UserType,
      args: {
        id: { type: new GraphQLNonNull(GraphQLString) },
      },
      resolve(parentValue, { id }) {
        return axios
          .delete(`http://localhost:3000/users/${id}`)
          .then((res) => res.data);
      },
    },
    editUser: {
      type: UserType,
      args: {
        id: { type: new GraphQLNonNull(GraphQLString) },
        firstName: { type: GraphQLString },
        age: { type: GraphQLInt },
        companyId: { type: GraphQLString },
      },
      resolve(parentValue, args) {
        return axios
          .patch(`http://localhost:3000/users/${args.id}`, args)
          .then((res) => res.data);
      },
    },
  },
});

module.exports = new GraphQLSchema({
  mutation,
  query: RootQuery,
});
KeihakuOh commented 2 months ago

GraphQL Expressについて

Expressサーバーは、複数のルート(例えば、/, /api, /graphql など)を設定できます。GraphQL Expressは、このルートの1つとしてGraphQLの機能を提供します。GraphQL Express自体はサーバではなく、Expressサーバの中で動作するルートミドルウェアです。したがって、GraphQL Expressだけではサーバとは言えません。GraphQL Expressを使うには、必ずExpressサーバが動作している必要があり、GraphQLはそのサーバの一部として機能します。

ーーーーーー より広義のサーバの定義: ソフトウェアサーバ: 例えば、ExpressやNode.jsなどのサーバアプリケーションは、ポート番号を監視し、HTTPリクエストを受け付けてレスポンスを返します。ポート番号をリッスンして、ネットワーク経由でアクセスできるサービスを提供する場合が多い。 物理サーバ: データセンターなどに設置され、複数のサーバアプリケーションを動作させるための実際のコンピュータ。 仮想サーバ: 物理サーバ上で仮想的に動作し、複数のサーバを1つの物理サーバで運用できるもの。

KeihakuOh commented 2 months ago
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

// Apollo Client を作成
const client = new ApolloClient({
  uri: 'https://example.com/graphql', // 自分のGraphQLサーバーのURL
  cache: new InMemoryCache()           // キャッシュの設定
});

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

Apollo Provider は、React コンポーネントに GraphQL サーバーと通信するための Apollo Client を使えるようにするためのもの。

KeihakuOh commented 2 months ago
import React, { Component } from 'react';
import gql from 'graphql-tag';
import { graphql } from 'react-apollo';

class SongList extends Component {
  renderSongs() {
    if (this.props.data.loading) {
      return <div>Loading...</div>;
    }

    return this.props.data.songs.map(song => {
      return <li key={song.title}>{song.title}</li>;
    });
  }

  render() {
    return (
      <div>
        <ul>
          {this.renderSongs()}
        </ul>
      </div>
    );
  }
}

const query = gql`
  {
    songs {
      title
    }
  }
`;

export default graphql(query)(SongList);

graphql(query) は Apollo Client が提供する HOC 関数を返します。 その返された HOC 関数に SongList を渡すことで、Apollo Client は SongList コンポーネントに自動的に GraphQL のデータフェッチ機能を追加します。 結果として、SongList コンポーネントは props を通してデータを受け取り、そのデータを表示できます。

↑これができるのはApolloProviderのおかげ

ちなみに、

<ApolloProvider client={client}>
      <SongList />
</ApolloProvider>

実際propsをゲットしているのはimportの後

KeihakuOh commented 1 month ago
import React, { Component } from 'react';
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';

class SongCreate extends Component {
  constructor(props) {
    super(props);

    this.state = { title: '' };
  }

  onSubmit(event) {
    event.preventDefault();

    this.props.mutate({
      variables: {
        title: this.state.title
      }
    });
  }

  render() {
    return (
      <div>
        <h3>Create a New Song</h3>
        <form onSubmit={this.onSubmit.bind(this)}>
          <label>Song Title:</label>
          <input
            onChange={event => this.setState({ title: event.target.value })}
            value={this.state.title}
          />
        </form>
      </div>
    );
  }
}

const mutation = gql
  mutation AddSong($title: String){
    addSong(title: $title) {
      title
    }
  }
;

export default graphql(mutation)(SongCreate);

    super(props);

    this.state = { title: '' };はそれぞれ何?また、    this.props.mutate({
      variables: {
        title: this.state.title
      }
    });

this.props.mutate: graphql HOC(Higher-Order Component)が提供するメソッドで、ミューテーションを実行するための関数です。 この mutate 関数は、Apollo Clientによって自動的に props に追加されます。 SongCreate コンポーネントに mutation がバインドされているので、mutate 関数が使えます。

variables: { title: this.state.title }: ミューテーションに渡す変数を指定します。この場合、title にユーザーがフォームに入力した this.state.title の値を渡しています。 このデータは、GraphQLサーバーに送信され、曲(Song)のタイトルとして新規登録されます。

ミューテーションの実行: AddSong ミューテーションが実行され、タイトルを持った新しい曲がバックエンドに追加されます。

KeihakuOh commented 1 month ago
import gql from 'graphql-tag';

export default gql`
  query SongQuery($id: ID!) {
    song(id: $id) {
      id
      title
    }
  }
`;

query SongQuery($id: ID!) のようにクエリ名をつけなくても行ける

KeihakuOh commented 1 month ago

dataIdFromObject: o => o.idがどのように機能するか説明

dataIdFromObjectの役割: オブジェクトのIDを使ってキャッシュのキーを決定し、データの一意性を確保します。 メリット: キャッシュが一貫性を保ち、同じデータが二重にキャッシュされるのを防ぎ、効率的な更新を実現します。

最初のresponse
{
  "data": {
    "user": {
      "id": "1",
      "name": "John Doe",
      "age": 30
    }
  }
}
キャッシュ:
{
  "1": {      // 1 がキャッシュキー
    "id": "1",
    "name": "John Doe",
    "age": 30
  }
}
次更新する時のresponse
{
  "data": {
    "updateUser": {
      "id": "1",
      "name": "Jane Doe",
      "age": 30
    }
  }
}
キャッシュ:
{
  "1": {      // 1 がキャッシュキー
    "id": "1",
    "name": "Jane Doe",
    "age": 30
  }
}
KeihakuOh commented 1 month ago
onSubmit({ email, password }) {
    this.props.mutate({
      variables: { email, password },
      refetchQueries: [{ query }]
    }).catch(res => {
      const errors = res.graphQLErrors.map(error => error.message);
      this.setState({ errors });
    });
  }

catch(res)は、Apollo Clientで返されるGraphQLエラーレスポンスに合わせたエラーハンドリング。 res.graphQLErrorsは、GraphQLのエラー情報が含まれるため、それを使ってエラーメッセージを抽出している。 通常のcatch(err)とは異なり、GraphQLのエラーレスポンス全体を処理する必要があるため、resという名前を使っているのです。 このように、GraphQLとApollo Client特有のエラーハンドリングのため、catch(res)を使っています。

KeihakuOh commented 1 month ago

export default graphql(query)(graphql(mutation)(LoginForm)); ここでは、まずgraphql(mutation)がLoginFormをラップし、その後にgraphql(query)がさらにその結果をラップします。 処理の順序: ミューテーション(graphql(mutation))のHOCが最初に適用され、LoginFormにミューテーション関連のpropsが渡されます。 その後、クエリ(graphql(query))のHOCが適用され、クエリの結果がpropsとして追加されます。

  1. クエリを待たずに、ミューテーションが先に設定される: LoginFormが最初にレンダリングされるとき、props.mutateはすでに利用可能になります。 ユーザーの情報を更新するミューテーションは、クエリの結果がなくてもトリガー可能です。

  2. クエリ結果が後からpropsに渡される: クエリ結果(props.data)は非同期で取得され、取得完了後にpropsが更新されます。 この場合、ミューテーションが先に実行されてもクエリの結果には依存しないシナリオに適しています。

ーーーーーーー export default graphql(mutation)(graphql(query)(LoginForm)); ここでは、まずgraphql(query)がLoginFormをラップし、その後にgraphql(mutation)がさらにその結果をラップします。 処理の順序: クエリ(graphql(query))のHOCが最初に適用され、LoginFormにクエリの結果がpropsとして渡されます。 その後、ミューテーション(graphql(mutation))のHOCが適用され、ミューテーション関連のpropsが追加されます。

  1. クエリの結果が最初に利用可能になる: LoginFormは、まずクエリの結果(例:props.data.user)を取得します。 この情報は、フォームの初期値の設定や、ミューテーションの前処理に使用できます。

  2. クエリ結果が揃った後にミューテーションがトリガーされる: ここでは、ユーザー情報の事前取得が完了してからミューテーションが使われるので、クエリの結果に依存する処理(フォーム入力の初期化など)に適しています。

KeihakuOh commented 1 month ago
import React, { Component } from 'react';
import AuthForm from './AuthForm';
import mutation from '../mutations/Login';
import { graphql } from 'react-apollo';
import query from '../queries/CurrentUser';
import { hashHistory } from 'react-router';

class LoginForm extends Component {
  constructor(props) {
    super(props);

    this.state = { errors: [] };
  }

  componentWillUpdate(nextProps) {
    // this.props // the old, current set of props
    // nextProps // the next set of props that will be in place
    // when the component rerenders
    if (!this.props.data.user && nextProps.data.user) {
      // redirect to dashboard!!!!
      hashHistory.push('/dashboard');
    }
  }

  onSubmit({ email, password }) {
    this.props.mutate({
      variables: { email, password },
      refetchQueries: [{ query }]
    }).catch(res => {
      const errors = res.graphQLErrors.map(error => error.message);
      this.setState({ errors });
    });
  }

  render() {
    return (
      <div>
        <h3>Login</h3>
        <AuthForm
          errors={this.state.errors}
          onSubmit={this.onSubmit.bind(this)}
        />
      </div>
    );
  }
}

export default graphql(query)(
  graphql(mutation)(LoginForm)
);

eactコンポーネントは、propsやstateが変化すると再レンダリングされます。 このコードの場合、props.dataの更新(特にdata.userの変更)は、LoginFormの再レンダリングを引き起こします。 例えば、ユーザーがログインに成功して、queryで取得されたuserオブジェクトがnullから有効なユーザーに変わった場合、それがnextProps.data.userに反映され、componentWillUpdateが呼ばれます。

props.dataは親に送信されず、LoginForm内でのみ使用される。親から子にpropsが渡されることはあっても、子から親にpropsを直接送信することはありません。