ReactとApolloでGraphQLを使った簡易Todoリストを作成してみた

**私は初心者です。ググりながら書きました、間違いなどあれば教えていただけるとありがたいです** :bow:

参考

- [Apollo なら爆速で GraphQL サーバーと GraphQL クライアントアプリが作れる - Qiira](https://qiita.com/jintz/items/4ddc6bf4f95238eff5e9)
- [React, Redux, GraphQLを学べるオンライン教材「Full Stack Open 2019」がとても良かった - fortkle blog](https://fortkle.hatenablog.com/entry/2019/12/11/095719)

---

![7WoZtWz864.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/8cf8bf05-3765-3d10-3d2c-a9d95ada8cc6.gif)

---

私が作成したコードは以下です

<a href="https://github.com/okumurakengo/react-apollo-todo-list-sample"><img src="https://github-link-card.s3.ap-northeast-1.amazonaws.com/okumurakengo/react-apollo-todo-list-sample.png" width="460px"></a>


# GraphQLサーバー作成

```src/server/index.ts
import { ApolloServer, UserInputError, gql } from 'apollo-server'
import { IResolvers } from 'graphql-tools'
import uuid from 'uuid/v1'
import { ITodo, FILTER } from '../global'

let filter: FILTER = FILTER.SHOW_ALL
let todos: ITodo[] = [
    { id: '6a679350-2aed-11ea-b2b6-89f5b6bcf823', text: '買い物', completed: false },
    { id: '6a679351-2aed-11ea-b2b6-89f5b6bcf823', text: '映画', completed: true, },
    { id: '6a679352-2aed-11ea-b2b6-89f5b6bcf823', text: 'ゲーム', completed: false, },
]

const typeDefs = gql`
    enum FILTER {
        SHOW_ALL
        SHOW_COMPLETED
        SHOW_ACTIVE
    }

    type Todo {
        id: ID!
        text: String!
        completed: Boolean!
    }

    type Query {
        allTodos: [Todo!]!
    }

    type Mutation {
        addTodo(text: String!): Todo!
        toggleCompleted(id: ID!): Todo!
        setFilter(filter: FILTER!): FILTER!
    }
`

const resolvers: IResolvers = {
    Query: {
        allTodos: () => {
            if (filter === 'SHOW_ALL') {
                return todos
            }
            return todos.filter(t => 
                filter === 'SHOW_COMPLETED' ? t.completed : !t.completed
            )
        },
    },
    Mutation: {
        addTodo: (root, args) => {
            const todo = { ...args, id: uuid(), completed: false }
            todos = [...todos, todo];
            return todo
        },
        toggleCompleted: (root, args) => {
            const todo = todos.find(t => t.id === args.id)
            if (!todo) {
                throw new UserInputError('id not found', {
                    invalidArgs: args.id,
                })
            }

            todo.completed = !todo.completed
            todos = todos.map(t => t.id === todo.id ? todo : t)
            return todo
        },
        setFilter: (root, args) => {
            return filter = args.filter
        },
    },
}

const server = new ApolloServer({
    typeDefs,
    resolvers,
})

server.listen().then(({ url }) => {
    console.log(`Server ready at ${url}`)
})
```

以下のように取得、更新メソッドをgraphqlで定義してみました

- allTodos : todo一覧取得
- addTodo : todo取得
- toggleCompleted : todoの完了状態を更新
- setFilter : 一覧取得するときのフィルター

---

vscodeを使っているのであれば、こちらのプラグインでシンタックスハイライトをつけてくれてみやすかったです。(※それ以外にも便利な機能があるプラグインです)

https://marketplace.visualstudio.com/items?itemName=Prisma.vscode-graphql

Before

![Screen Shot 2019-12-31 at 1.44.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/ce319172-986e-f0d3-64d5-cdd552465981.png)

After

![Screen Shot 2019-12-31 at 1.44.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/d8641b76-979d-ea2b-6874-eea6d7b68c60.png)

# ReactでTodoリスト作成

```tsx:src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
import ApolloClient from 'apollo-boost'
import { ApolloProvider } from "@apollo/react-hooks"

const client = new ApolloClient({
    uri: 'http://localhost:4000/graphql'
})

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

```tsx:src/components/App.tsx
import React from 'react';
import { useQuery, useMutation } from '@apollo/react-hooks'
import { gql } from 'apollo-boost'
import AddTodo from "./AddTodo";
import TodoList from "./TodoList";
import Footer from "./Footer";

export const ALL_TODOS = gql`
  query {
    allTodos {
      id
      text
      completed
    }
  }
`

const ADD_TODO = gql`
  mutation addTodo($text: String!) {
    addTodo(text: $text) {
      id
      text
      completed
    }
  }
`

const SET_FILTER = gql`
  mutation setFilter($filter: FILTER!) {
    setFilter(filter: $filter)
  }
`

const App: React.FC = () => {
  const { loading, data } = useQuery(ALL_TODOS)

  const [addTodo] = useMutation(ADD_TODO, {
    refetchQueries: [{ query: ALL_TODOS }]
  })

  const [toggleCompleted] = useMutation(SET_FILTER, {
    refetchQueries: [{ query: ALL_TODOS }]
  })

  return (
    <>
      <AddTodo addTodo={addTodo} />
      <TodoList {...{ loading, data }} />
      <Footer toggleCompleted={toggleCompleted} />
    </>
  );
}

export default App;
```

`useQuery`, `useMutation` を使って、graphqlのサーバーと通信、通信のための関数を取得しています。

`addTodo` を実行した直後に、todo一覧を再び取得したかったので、`refetchQueries: [{ query: ALL_TODOS }]` と指定するとうまく、`addTodo`実行後 ->　`allTodos` を実行してくれるようでした

---

```tsx:src/components/AddTodo.tsx
import React, { useState } from "react";

interface IProps {
    addTodo: (arg: any) => any;
}

const AddTodo: React.FC<IProps> = ({ addTodo }): JSX.Element => {
    const [text, setText] = useState<string>("");

    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        if (!text.trim()) return;
        addTodo({
            variables: { text }
        })
        setText("");
    };

    return (
        <form onSubmit={handleSubmit}>
            <input value={text} onChange={e => setText(e.target.value)} />
            <button type="submit">Add Todo</button>
        </form>
    );
}

export default AddTodo
```

フォームがサブミットされた時のコールバック関数で`addTodo`を実行し、todoを追加します。

propsの型定義を `addTodo: (arg: any) => any;` としたのですが、どう書くのいいのかわかりませんので、詳しい方教えていただけるとありがたいです :bow:

---

```tsx:src/components/TodoList.tsx
import * as React from "react";
import { useApolloClient } from '@apollo/react-hooks'
import { gql } from 'apollo-boost'
import { ITodo } from '../global'

import { ALL_TODOS } from './App'

const TOGGLE_COMPLETED = gql`
  mutation toggleCompleted($id: ID!) {
    toggleCompleted(id: $id) {
      id
      text
      completed
    }
  }
`

interface IPorps {
    loading: boolean;
    data: any;
}

const TodoList: React.FC<IPorps> = ({ loading, data }): JSX.Element => {
    const client = useApolloClient()

    if (loading) {
        return <div>loading...</div>
    }

    const toggleCompleted = (id: string) => {
        client.mutate({
            mutation: TOGGLE_COMPLETED,
            variables: { id },
            refetchQueries: [{ query: ALL_TODOS }],
        })
    }

    return (
        <ul>
            {data && data.allTodos.map((t: ITodo) => 
                <li
                    key={t.id}
                    style={{ textDecoration: t.completed ? 'line-through' : 'none' }}
                    onClick={() => toggleCompleted(t.id)}
                >
                    {t.text}
                </li>
            )}
        </ul>
    )
}

export default TodoList
```

ここでは、`const client = useApolloClient()` で取得した`client`で`client.mutate`とし、graphqlの`Mutation`で定義した部分へ通信しています。

graphqlの`Query`で定義した部分へ通信する場合は、`client.query` とする良いようです。

---

```tsx:src/components/Footer.tsx
import * as React from "react";
import { FILTER } from '../global'

interface IProps {
    toggleCompleted: (arg: any) => any;
}

const Footer: React.FC<IProps> = ({ toggleCompleted }): JSX.Element => {
    return (
        <>
            <span>Show:</span>
            <button onClick={() => toggleCompleted({ variables: { filter: FILTER.SHOW_ALL }})}>
                ALL
            </button>
            <button onClick={() => toggleCompleted({ variables: { filter: FILTER.SHOW_COMPLETED }})}>
                Active
            </button>
            <button onClick={() => toggleCompleted({ variables: { filter: FILTER.SHOW_ACTIVE }})}>
                Completed
            </button>
        </>
    )
}

export default Footer;
```

todoの一覧表示するフィルターを変更します

---

あまりわかってないのですが動かせるものを作ることができました。最後まで見ていただいてありがとうございました :bow: