간단한 Hasura GraphiQL2

94412 단어 GraphQLtech
지난번 기고문에서는 하수라로 API 서버를 구축하는 단계까지 진행됐다.
https://zenn.dev/mrsung/articles/75cac31621bb6e
이번에는 실제로 그 단점을 CRUD 처리해 보았다.

기술 스택


스노우팩으로 리액트+타입 스크립트+아폴로의 환경을 만들었다.
스노우팩 공식에는 설치에 대한 자세한 내용이 적혀 있다.
https://www.snowpack.dev/tutorials/react
다음 템플릿은 React+Type Script에서 사용됩니다.
https://github.com/snowpackjs/snowpack/tree/main/create-snowpack-app/app-template-react-typescript

UI 만들기

index.html에는 읽기만 해도 기분이 좋은 css가 추가됐다.
water.css 좋아해요.
https://github.com/kognise/water.css
먼저 외관을 추가합니다.
추가 TODO용 input을 controlled component로 만들어야 하기 때문이다.
index.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import { App } from './app'

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
)
app.tsx
import React, { useState } from 'react'

export const App = () => {
  const [todoItemText, setTodoItemText] = useState('')

  return (
    <div>
      <h1>GraphQL Checklist</h1>
      {/* Todo form */}
      <form>
        <input
          type='text'
          name='todo'
          id='todo'
          placeholder='Write your todo'
          value={todoItemText}
          onChange={ev => setTodoItemText(ev.target.value)}
        />
        <button type='submit'>Add</button>
      </form>
      {/* Todo list */}
      <ul>
        <li>
          <span style={{ display: 'inline-block', marginRight: '1em' }}>
            todo item 1
          </span>
          <button type='button'>&#10003;</button>
          <button type='button'>&times;</button>
        </li>
      </ul>
    </div>
  )
}

Apollo client 설정


이곳의 절차는 공식 문서를 보는 것이다.
https://www.apollographql.com/docs/react/get-started/
원하는 매크로 패키지를 추가합니다.
yarn add @apollo/client graphql
Apollo client 준비
공식은 다음과 같은client 를 정의했다
import { ApolloClient, InMemoryCache } from '@apollo/client'

const client = new ApolloClient({
  uri: 'https://48p1r2roz4.sse.codesandbox.io',
  cache: new InMemoryCache()
})
api키를 헤더에 추가할 때 다음과 같습니다.
import { createHttpLink, ApolloClient, InMemoryCache } from '@apollo/client'

const httpLink = createHttpLink({
  uri: 'https://your.hasura.app/v1/graphql',
  headers: {
    'x-hasura-admin-secret': YOUR_HASURA_ADMIN_SECRET
  }
})

export const client = new ApolloClient({
  link: httpLink,
  cache: new InMemoryCache()
})
index.tsx측으로provide를 진행한다.
index.tsx
import React from 'react'
import ReactDOM from 'react-dom'
+ import { ApolloProvider } from '@apollo/client'
+ import { App, client } from './app'

ReactDOM.render(
+   <ApolloProvider client={client}>
+     <React.StrictMode>
+       <App />
+     </React.StrictMode>
+   </ApolloProvider>,
  document.getElementById('root')
)
app.tsx
import React, { useState } from 'react'
+ import { createHttpLink, ApolloClient, InMemoryCache } from '@apollo/client'

+ const httpLink = createHttpLink({
+   uri: 'https://your.hasura.app/v1/graphql',
+   headers: {
+     'x-hasura-admin-secret': YOUR_HASURA_ADMIN_SECRET
+   }
+ })

export const client = new ApolloClient({
  link: httpLink,
  cache: new InMemoryCache()
})

export const App = () => {
  const [todoItemText, setTodoItemText] = useState('')

  return (
    <div>
      <h1>GraphQL Checklist</h1>
      {/* Todo form */}
      <form>
        <input
          type='text'
          name='todo'
          id='todo'
          placeholder='Write your todo'
          value={todoItemText}
          onChange={ev => setTodoItemText(ev.target.value)}
        />
        <button type='submit'>Add</button>
      </form>
      {/* Todo list */}
      <ul>
        <li>
          <span style={{ display: 'inline-block', marginRight: '1em' }}>
            todo item 1
          </span>
          <button type='button'>&#10003;</button>
          <button type='button'>&times;</button>
        </li>
      </ul>
    </div>
  )
}

GraphiQL 쿼리 추가


질의를 정의합니다.
app.tsx
import React, { useState } from 'react'
import {
    createHttpLink,
    ApolloClient,
    InMemoryCache,
+   gql
} from '@apollo/client'

const httpLink = createHttpLink({
  uri: 'https://your.hasura.app/v1/graphql',
  headers: {
    'x-hasura-admin-secret': YOUR_HASURA_ADMIN_SECRET
  }
})

export const client = new ApolloClient({
  link: httpLink,
  cache: new InMemoryCache()
})

+ const GET_TODOS = gql`
+   query getTodos {
+     todos {
+       done
+       id
+       text
+     }
+   }
+ `

+ const TOGGLE_TODO = gql`
+   mutation toggleTodo($id: uuid!, $done: Boolean!) {
+     update_todos(where: { id: { _eq: $id } }, _set: { done: $done }) {
+       returning {
+         done
+         id
+         text
+       }
+     }
+   }
+ `

+ const ADD_TODO = gql`
+   mutation addTodo($text: String!) {
+     insert_todos(objects: { text: $text }) {
+       returning {
+         done
+         id
+         text
+       }
+     }
+   }
+ `

+ const DELETE_TODO = gql`
+   mutation deleteTodos($id: uuid!) {
+     delete_todos(where: { id: { _eq: $id } }) {
+       returning {
+         done
+         id
+         text
+       }
+     }
+   }
+ `

export const App = () => {
  const [todoItemText, setTodoItemText] = useState('')

  return (
    <div>
      <h1>GraphQL Checklist</h1>
      {/* Todo form */}
      <form>
        <input
          type='text'
          name='todo'
          id='todo'
          placeholder='Write your todo'
          value={todoItemText}
          onChange={ev => setTodoItemText(ev.target.value)}
        />
        <button type='submit'>Add</button>
      </form>
      {/* Todo list */}
      <ul>
        <li>
          <span style={{ display: 'inline-block', marginRight: '1em' }}>
            todo item 1
          </span>
          <button type='button'>&#10003;</button>
          <button type='button'>&times;</button>
        </li>
      </ul>
    </div>
  )
}

CRUD 프로세스 추가


Apollo clientuseQueryuseMutation에 query와mutation을 각각 기술합니다.
추가 처리는 완성형에 쓴 후 다음과 같다.
app.tsx
import React, { useState } from 'react'
import {
    createHttpLink,
    ApolloClient,
    InMemoryCache,
    gql,
+   useQuery,
+   useMutation
} from '@apollo/client'

const httpLink = createHttpLink({
  uri: 'https://your.hasura.app/v1/graphql',
  headers: {
    'x-hasura-admin-secret': YOUR_HASURA_ADMIN_SECRET
  }
})

export const client = new ApolloClient({
  link: httpLink,
  cache: new InMemoryCache()
})

const GET_TODOS = gql`
  query getTodos {
    todos {
      done
      id
      text
    }
  }
`

const TOGGLE_TODO = gql`
  mutation toggleTodo($id: uuid!, $done: Boolean!) {
    update_todos(where: { id: { _eq: $id } }, _set: { done: $done }) {
      returning {
        done
        id
        text
      }
    }
  }
`

const ADD_TODO = gql`
  mutation addTodo($text: String!) {
    insert_todos(objects: { text: $text }) {
      returning {
        done
        id
        text
      }
    }
  }
`

const DELETE_TODO = gql`
  mutation deleteTodos($id: uuid!) {
    delete_todos(where: { id: { _eq: $id } }) {
      returning {
        done
        id
        text
      }
    }
  }
`

+ interface ITodo {
+   done: boolean
+   id: string
+   text: string
+ }

+ interface IGetTodosData {
+   todos: ITodo[]
+ }

export const App = () => {
  const [todoItemText, setTodoItemText] = useState('')

+ const { data, loading, error } = useQuery<IGetTodosData>(GET_TODOS)
+ const [toggleTodo] = useMutation(TOGGLE_TODO)
+ const [addTodo] = useMutation(ADD_TODO, {
+   onCompleted: () => setTodoItemText('')
+ })
+ const [deleteTodo] = useMutation(DELETE_TODO)

+ const handleAddTodo = async (ev: React.FormEvent<HTMLFormElement>) => {
+   ev.preventDefault()

+   if (!todoItemText.trim()) return

+   await addTodo({
+     variables: { text: todoItemText },
+     refetchQueries: [{ query: GET_TODOS }]
+   })
+ }
+ const handleToggleTodo = async ({ id, done }: ITodo) => {
+   await toggleTodo({ variables: { id: id, done: !done } })
+ }
+ const handleDeleteTodo = async ({ id }: ITodo) => {
+   if (!window.confirm('Do you want to delete this todo?')) {
+     return
+   }

+     await deleteTodo({
+       variables: { id },
+       update: cache => {
+         const prevData = cache.readQuery<IGetTodosData>({ query: GET_TODOS })
+         if (!prevData) return

+         const newTodos = prevData.todos.filter(todo => todo.id !== id)
+         cache.writeQuery({ query: GET_TODOS, data: { todos: newTodos } })
+       }
+     })
+   }

+   if (loading) return <div>loading...</div>

+   if (error) {
+     console.error(error)

+     return <div>Error fetching todos!</div>
+   }

  return (
    <div>
      <h1>GraphQL Checklist</h1>
      {/* Todo form */}
 +     <form onSubmit={ev => handleAddTodo(ev)}>
+        <input
          type='text'
          name='todo'
          id='todo'
          placeholder='Write your todo'
+         value={todoItemText}
+         onChange={ev => setTodoItemText(ev.target.value)}
        />
        <button type='submit'>Add</button>
      </form>
      {/* Todo list */}
      <ul>
 +         {typeof data !== 'undefined' &&
+           data.todos.map(todo => (
+             <li key={todo.id}>
+               <span style={{ textDecoration: todo.done ? 'line-through' : '' }}>
+                 {todo.text}
+               </span>{' '}
+               <button type='button' onClick={() => handleToggleTodo(todo)}>
+                 &#10003;
+               </button>
+               <button type='button' onClick={() => handleDeleteTodo(todo)}>
+                 &times;
+               </button>
+             </li>
+           ))}
      </ul>
    </div>
  )
}
완성형의 외관은 다음과 같다.

하수라 콘솔의GraphiQL에서는 로컬에서mutation을 실행한 후 DB에 저장된 TODO 일람표를 확인할 수 있다.

중점적 인 실장 을 묘사하고 있다


유형 정의


interface ITodo {
  done: boolean
  id: string
  text: string
}

interface IGetTodosData {
  todos: ITodo[]
}
Hasura console에서 테이블을 만들 때 정의된 유형의 에뮬레이션입니다.취득한 TODO 데이터의 유형을 나타냅니다.

useQuery API


const GET_TODOS = gql`
  query getTodos {
    todos {
      done
      id
      text
    }
  }
`

// 中略

const { data, loading, error } = useQuery<IGetTodosData>(GET_TODOS)
Apollo 응용 프로그램에서query를 실행하는 주요 API 형식은 React hooks입니다.gqlquery documents에 있는 의류useQuery로 기능을 해석하면 data,loading,error를 얻을 수 있다.loadingerrorfalse면query가 완성됩니다.
https://www.apollographql.com/docs/react/data/queries/
https://www.apollographql.com/docs/react/api/react/hooks/#usequery
공식 예에서는 GraphiQL variables를 전달하여 클라이언트로부터 변수의 값을 지정할 수도 있습니다.
const GET_DOG_PHOTO = gql`
  query Dog($breed: String!) {
    dog(breed: $breed) {
      id
      displayImage
    }
  }
`

function DogPhoto({ breed }) {
  const { loading, error, data } = useQuery(GET_DOG_PHOTO, {
    variables: { breed }
  })

  if (loading) return null
  if (error) return `Error! ${error}`

  return <img src={data.dog.displayImage} style={{ height: 100, width: 100 }} />
}

useMutation API


const TOGGLE_TODO = gql`
  mutation toggleTodo($id: uuid!, $done: Boolean!) {
    update_todos(where: { id: { _eq: $id } }, _set: { done: $done }) {
      returning {
        done
        id
        text
      }
    }
  }
`

const ADD_TODO = gql`
  mutation addTodo($text: String!) {
    insert_todos(objects: { text: $text }) {
      returning {
        done
        id
        text
      }
    }
  }
`

const DELETE_TODO = gql`
  mutation deleteTodos($id: uuid!) {
    delete_todos(where: { id: { _eq: $id } }) {
      returning {
        done
        id
        text
      }
    }
  }
`

// 中略

const [toggleTodo] = useMutation(TOGGLE_TODO)
const [addTodo] = useMutation(ADD_TODO, {
  onCompleted: () => setTodoText('')
})
const [deleteTodo] = useMutation(DELETE_TODO)
useQuery에서 데이터를 얻었습니다.useMutation에서 데이터를 업데이트할 수 있습니다.useMutation에서는mutation을 실행하는mutate function과 현재mutation이 실행하는 현재 상태를 표시하는 대상을 얻을 수 있습니다.onCompleted에서mutation의callback 함수를 지정합니다.
Apollo client는query의 결과를 로컬 캐시하는 기구가 있지만 Apollo client의cache는mutation의 결과를 자동으로 업데이트하지 않기 때문에 적절한 처리가 필요합니다.
https://www.apollographql.com/docs/react/data/mutations/
https://www.apollographql.com/docs/react/api/react/hooks/#usemutation

실행


const handleAddTodo = async (ev: React.FormEvent<HTMLFormElement>) => {
  ev.preventDefault()

  if (!todoText.trim()) return

  await addTodo({
    variables: { text: todoText },
    refetchQueries: [{ query: GET_TODOS }]
  })
}

const handleToggleTodo = async ({ id, done }: ITodo) => {
  await toggleTodo({ variables: { id: id, done: !done } })
}

const handleDeleteTodo = async ({ id }: ITodo) => {
  if (!window.confirm('Do you want to delete this todo?')) {
    return
  }

  await deleteTodo({
    variables: { id },
    update: cache => {
      const prevData = cache.readQuery<IGetTodosData>({ query: GET_TODOS })
      if (!prevData) return

      const newTodos = prevData.todos.filter(todo => todo.id !== id)
      cache.writeQuery({ query: GET_TODOS, data: { todos: newTodos } })
    }
  })
}
useMutation에서 얻은mutate function을 실행합니다.
위에서 말한 바와 같이mutation 결과를 다시 캐시해야 하기 때문에refetchQueriesupdate에서 처리합니다.refetchQueries에서mutation을 실행한 후에 다시 가져올 검색어를 지정합니다.update 캐시를 업데이트하는 함수를 실행합니다.

총결산


이번에 하수라와 리액트+아폴로 클라이언트에서 크루 처리가 이뤄졌다.
Hasura를 사용하고 싶어요.GraphiQL과 그 생태계에 조금 익숙해져요.

좋은 웹페이지 즐겨찾기