React + GraphQL + Apolloを触ってみる ~ 4. ToDoの追加・編集・削除まで ~
前回からの続き
ToDoの追加、編集、削除機能を実装する
目次
- GraphQLの環境構築まで
- ToDoリストの読み込みまで
- Firebaseの準備とGraphQLとの連携部分まで
- ToDoの追加・編集・削除まで
サーバ側
GraphQLで編集を行う場合、QueryではなくMutationを利用するため、Mutation用のファイルを新たに作成する
import { collection, doc, addDoc, setDoc, deleteDoc } from 'firebase/firestore/lite'
import { db } from '../utils/firebase.js'
export const Mutation = {
addTodo: async (parent, args, context, info) => {
const { text } = args
await addDoc(collection(db, 'todos'), { text, isCompleted: false, })
return true
},
updateTodo: async(parent, args, context, info) => {
const { id, text, isCompleted } = args
await setDoc(doc(db, 'todos', id), { text, isCompleted })
return true
},
deleteTodo: async(parent, args, context, info) => {
const { id } = args
await deleteDoc(doc(db, 'todos', id))
return true
}
}
上記のコードはすでにFirebaseへの操作も実装済み
昔よりFirebaseシンプルに書けるようになったなー
あと今回はシンプルに実装するためにエラー処理は省略してる
resolversへの追加とスキーマの追加も忘れずに
import { Query } from './resolvers/query.js'
+ import { Mutation } from './resolvers/mutations.js'
- const resolvers = { Query }
+ const resolvers = { Query, Mutation }
export default resolvers
const typeDefs = `#graphql
type Todo {
id: ID!,
text: String,
isCompleted: Boolean,
}
type Query {
getTodos: [Todo],
}
+ type Mutation {
+ addTodo(text: String): Boolean
+ updateTodo(id: ID!, text: String, isCompleted: Boolean): Boolean
+ deleteTodo(id: ID!): Boolean
+ }
`
export default typeDefs
フロント側
ToDoの追加、編集、削除を呼び出すよう修正
これは完全な好みだけど、追加とか編集とかするならContextにまとめたい欲があるので、取得部分も合わせて全部Contextに押し込むようにする
取得部分をContext化
まずは取得部分のコードをContextで作成
import { useQuery } from '@apollo/client'
import { createContext, useState, useEffect } from 'react'
import { TODOS_QUERY } from '@/queries/todos'
const TodosContext = createContext()
const TodosProvider = ({ children }) => {
const [todos, setTodos] = useState([])
const { loading, error, data } = useQuery(TODOS_QUERY)
useEffect(() => {
if (!!data && !!data.getTodos) {
setTodos(data.getTodos)
}
}, [data])
return <TodosContext.Provider value={{ todos, loading, error }}>{children}</TodosContext.Provider>
}
export { TodosContext, TodosProvider }
Providerでラップするのを忘れずに
import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client'
import { AppProps } from 'next/app'
import { createGlobalStyle } from 'styled-components'
import reset from 'styled-reset'
+ import { TodosProvider } from '@/contexts/todos'
const cache = new InMemoryCache()
const client = new ApolloClient({
uri: 'http://localhost:4001',
cache,
})
const GlobalStyle = createGlobalStyle`
& {
${reset}
* {
box-sizing: border-box;
}
}
`
function MyApp({ Component, pageProps }: AppProps) {
return (
<ApolloProvider client={client}>
+ <TodosProvider>
<GlobalStyle />
<Component {...pageProps} />
+ </TodosProvider>
</ApolloProvider>
)
}
export default MyApp
取得部分をContextを使うよう修正
import { NextPage } from 'next'
- import { useQuery } from '@apollo/client'
- import { TODOS_QUERY } from '@/queries/todos'
+ import { useContext } from 'react'
+ import { TodosContext } from '@/contexts/todos'
interface IndexPageProps {}
const IndexPage: NextPage<IndexPageProps> = (props: IndexPageProps) => {
- const { loading, error, data } = useQuery(TODOS_QUERY)
+ const { loading, error, todos } = useContext(TodosContext)
if (loading) {
return <div>loading..</div>
}
if (error) {
return <div>error: {error.message}</div>
}
- if (!data.getTodos) {
+ if (!todos) {
return null
}
return (
<ul>
- {data.getTodos.map(({ id, text, isCompleted }) => (
+ {todos.map(({ id, text, isCompleted }) => (
<li key={id}>
<div>id: {id}</div>
<div>text: {text}</div>
<div>isCompleted: {isCompleted ? 'true' : 'false'}</div>
</li>
))}
</ul>
)
}
export default IndexPage
追加、編集、削除機能の実装
Mutationの作成
import { gql } from '@apollo/client'
export const ADD_TODO_MUTATION = gql`
mutation addTodo($text: String!) {
addTodo(text: $text)
}
`
export const UPDATE_TODO_MUTATION = gql`
mutation updateTodo($id: ID!, $text: String, $isCompleted: Boolean) {
updateTodo(id: $id, text: $text, isCompleted: $isCompleted)
}
`
export const DELETE_TODO_MUTATION = gql`
mutation deleteTodo($id: ID!) {
deleteTodo(id: $id)
}
`
Contextの修正
diffにするのが面倒だったので修正後のコードを載せとく
import { useQuery, useMutation } from '@apollo/client'
import { createContext, useState, useCallback, useEffect } from 'react'
import { TODOS_QUERY } from '../queries/todos'
import { ADD_TODO_MUTATION, UPDATE_TODO_MUTATION, DELETE_TODO_MUTATION } from '../mutations/todos'
const TodosContext = createContext()
const TodosProvider = ({ children }) => {
const [todos, setTodos] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState({})
const { loading: getLoading, error: getError, data } = useQuery(TODOS_QUERY)
const [_addTodo, { loading: addLoading, error: addError }] = useMutation(ADD_TODO_MUTATION, {
refetchQueries: ['getTodos'],
})
const [_updateTodo, { loading: updateLoading, error: updateError }] = useMutation(UPDATE_TODO_MUTATION, {
refetchQueries: ['getTodos'],
})
const [_deleteTodo, { loading: deleteLoading, error: deleteError }] = useMutation(DELETE_TODO_MUTATION, {
refetchQueries: ['getTodos'],
})
const addTodo = useCallback(
({ text }) => {
_addTodo({ variables: { text } })
},
[todos],
)
const updateTodo = useCallback(
({ id, text, isCompleted }) => {
_updateTodo({ variables: { id, text, isCompleted } })
},
[todos],
)
const deleteTodo = useCallback(
({ id }) => {
_deleteTodo({ variables: { id } })
},
[todos],
)
useEffect(() => {
if (!!data && !!data.getTodos) {
setTodos(data.getTodos)
}
}, [data])
useEffect(() => {
setLoading(getLoading || addLoading || updateLoading || deleteLoading)
setError({ getError, addError, updateError, deleteError })
}, [getLoading, addLoading, updateLoading, deleteLoading, getError, addError, updateError, deleteError])
return (
<TodosContext.Provider value={{ todos, loading, error, addTodo, updateTodo, deleteTodo }}>
{children}
</TodosContext.Provider>
)
}
export { TodosContext, TodosProvider }
useMutation
を使ってadd, update, deleteメソッドを実装
ポイントは refetchQueries
オプションを使って、更新後に再度取得するようにしている
これにより、更新があった場合に即座に一覧部分に反映されるようになる
useCallback
で各メソッドを再定義しているが、これはuseMutation
で作成したメソッドを呼び出す際に変数をvariables
オブジェクトで囲う必要があったので、毎回呼び出し先で囲う必要が無いようにしているだけで必須ではない
コンポーネントからの呼び出し
これも長くなったので修正後のコード
実際に使う際はコンポーネント分けよう(面倒だった
Contextを使うことで、コンポーネントが分かれていても更新後の一覧への自動反映もちゃんと動作するので良き
import { NextPage } from 'next'
import { useState, useContext } from 'react'
import { TodosContext } from '@/contexts/todos'
interface IndexPageProps {}
const IndexPage: NextPage<IndexPageProps> = (props: IndexPageProps) => {
const [text, setText] = useState('')
const { todos, loading, error, addTodo, updateTodo, deleteTodo } = useContext(TodosContext)
if (loading) {
return <div>loading..</div>
}
if (error.getError) {
return <div>error: {(error.getError || {}).message}</div>
}
if (!todos) {
return null
}
return (
<div>
<input type="text" value={text} onChange={e => setText(e.target.value)} />
<button onClick={(e) => {
e.preventDefault()
if (text.length < 0) return
addTodo({ text })
setText('')
}}>Add</button>
<hr />
<table>
<tbody>
{todos.map(({ id, text, isCompleted }) => (
<tr key={id}>
<td>
<input type="checkbox" checked={isCompleted} onChange={(e) => {
e.preventDefault()
updateTodo({ id, text, isCompleted: !isCompleted })
}} />
</td>
<td>{text}</td>
<td>
<button onClick={e => {
e.preventDefault()
deleteTodo({ id })
}}>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
export default IndexPage
これでGraphQLを使ってToDoリストっぽいものを作るという目的は果たせたので一旦はここまで
Discussion