yicru / tsuqrea-lt_20230614

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

爆速でGraphQLサーバーを立てたい!!!

2023-06-14LT資料@TSUQREA

Next.jsプロジェクトの作成

npx create-next-app@latest web --ts --eslint --app --src-dir --tailwind --import-alias "@/*"

prettierの導入

npm install -D prettier
echo '{"singleQuote": true, "semi": false}' > .prettierrc
echo '.next' >> .prettierignore
echo $(jq '.scripts.format="prettier --write ."' package.json) > package.json

GraphQl Yogaのインストール

npm install graphql-yoga graphql

GraphQL セットアップ

https://the-guild.dev/graphql/yoga-server/docs/integrations/integration-with-nextjs#example

mkdir -p src/app/api/graphql && touch src/app/api/graphql/route.ts
// src/app/api/graphql/route.ts
import { createYoga, createSchema } from 'graphql-yoga'
 
const { handleRequest } = createYoga({
  schema: createSchema({
    typeDefs: /* GraphQL */ `
      type Query {
        greetings: String
      }
    `,
    resolvers: {
      Query: {
        greetings: () =>
          'This is the `greetings` field of the root `Query` type'
      }
    }
  }),
 
  // While using Next.js file convention for routing, we need to configure Yoga to use the correct endpoint
  graphqlEndpoint: '/api/graphql',
 
  // Yoga needs to know how to create a valid Next response
  fetchAPI: { Response }
})
 
export { handleRequest as GET, handleRequest as POST }

Query,Mutationの追加

// src/app/api/graphql/route.ts
import { createYoga, createSchema } from 'graphql-yoga'

const todos = [
  { id: 1, text: 'Buy milk', completed: false },
  { id: 2, text: 'Buy eggs', completed: false },
  { id: 3, text: 'Buy bread', completed: false },
]

const { handleRequest } = createYoga({
  schema: createSchema({
    typeDefs: /* GraphQL */ `
      type Todo {
        id: ID!
        text: String!
        completed: Boolean!
      }

      type Query {
        greetings: String
        todos: [Todo!]!
      }

      type Mutation {
        addTodo(text: String!): Todo!
        toggleTodo(id: ID!): Todo!
      }
    `,
    resolvers: {
      Query: {
        greetings: () =>
          'This is the `greetings` field of the root `Query` type',
        todos: () => todos,
      },
      Mutation: {
        addTodo: (_, { text }) => {
          const todo = { id: todos.length + 1, text, completed: false }
          todos.push(todo)
          return todo
        },
        toggleTodo: (_, { id }) => {
          const todo = todos.find((todo) => todo.id == id)
          if (!todo) throw new Error(`Todo with id ${id} not found`)
          todo.completed = !todo.completed
          return todo
        },
      },
    },
  }),

  // While using Next.js file convention for routing, we need to configure Yoga to use the correct endpoint
  graphqlEndpoint: '/api/graphql',

  // Yoga needs to know how to create a valid Next response
  fetchAPI: { Response },
})

export { handleRequest as GET, handleRequest as POST }

GraphlQL Code Generatorの導入

npm i -D ts-node @graphql-codegen/cli @graphql-codegen/client-preset npm-run-all
touch codegen.ts
// codegen.ts
import { CodegenConfig } from '@graphql-codegen/cli'

const config: CodegenConfig = {
  schema: 'http://localhost:3000/api/graphql',
  documents: ['src/**/*.tsx'],
  ignoreNoDocuments: true, // for better experience with the watcher
  generates: {
    './src/lib/gql/': {
      preset: 'client',
    },
  },
}

export default config
{
  "dev": "run-p dev:*",
  "dev:next": "next dev",
  "dev:codegen": "graphql-codegen --watch"
}

urqlの導入

npm install urql
mkdir -p src/providers && touch src/providers/index.tsx
// src/providers/index.tsx
'use client'

import { ReactNode } from 'react'
import { Client, cacheExchange, fetchExchange, Provider } from 'urql'

type Props = {
  children: ReactNode
}

const client = new Client({
  url: '/api/graphql',
  exchanges: [cacheExchange, fetchExchange],
})

export const Providers = ({ children }: Props) => {
  return <Provider value={client}>{children}</Provider>
}
// src/app/layout.tsx
import './globals.css'
import { Inter } from 'next/font/google'
import { Providers } from '@/providers'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

UI実装

npx shadcn-ui init # tailwind.config.jsのcontentを修正
npx shadcn-ui add button input checkbox label
mkdir -p src/features/todo/components
touch src/features/todo/components/AddTodoForm.tsx src/features/todo/components/TodoListItem.tsx

https://ui.shadcn.com/docs/forms/react-hook-form

// src/app/layout.tsx
import './globals.css'
import { Inter } from 'next/font/google'
import { Providers } from '@/providers'
import { cn } from '@/lib/utils'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en" className={'h-full'}>
      <body className={cn(inter.className, 'h-full')}>
        <Providers>
          <div className={'h-full w-full max-w-lg mx-auto p-8'}>{children}</div>
        </Providers>
      </body>
    </html>
  )
}
// src/app/page.tsx
'use client'

import { graphql } from '@/lib/gql'
import { useQuery } from 'urql'
import { TodoListItem } from '@/features/todo/components/TodoListItem'
import { AddTodoForm } from '@/features/todo/components/AddTodoForm'

const HomePageQuery = graphql(/* GraphQL */ `
  query HomePageQuery {
    todos {
      id
      ...TodoListItem_todo
    }
  }
`)

export default function Home() {
  const [{ data }] = useQuery({ query: HomePageQuery })

  return (
    <main className={'flex flex-col h-full'}>
      <div className={'flex-1'}>
        <div className={'divide-y'}>
          {data?.todos.map((todo) => (
            <TodoListItem todo={todo} key={todo.id} />
          ))}
        </div>
      </div>
      <AddTodoForm />
    </main>
  )
}
// src/features/components/TodoListItem.tsx
import { Checkbox } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label'
import { cn } from '@/lib/utils'

import { FragmentType, graphql, useFragment } from '@/lib/gql'
import { useMutation } from 'urql'

const TodoFragment = graphql(/* GraphQL */ `
  fragment TodoListItem_todo on Todo {
    id
    text
    completed
  }
`)

const ToggleTodoMutation = graphql(/* GraphQL */ `
  mutation ToggleTodoMutation($id: ID!) {
    toggleTodo(id: $id) {
      id
      completed
    }
  }
`)

type Props = {
  todo: FragmentType<typeof TodoFragment>
}

export const TodoListItem = (props: Props) => {
  const todo = useFragment(TodoFragment, props.todo)
  const [, toggleTodo] = useMutation(ToggleTodoMutation)

  const handleOnChange = async () => {
    await toggleTodo({
      id: todo.id,
    })
  }

  return (
    <div className="flex items-center space-x-2 py-4">
      <Checkbox
        checked={todo.completed}
        id={`todo-${todo.id}`}
        onCheckedChange={handleOnChange}
      />
      <Label
        className={cn(todo.completed && 'line-through')}
        htmlFor={`todo-${todo.id}`}
      >
        {todo.text}
      </Label>
    </div>
  )
}
// src/features/components/AddTodoForm.tsx
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { zodResolver } from '@hookform/resolvers/zod'
import * as z from 'zod'
import { useForm } from 'react-hook-form'
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormMessage,
} from '@/components/ui/form'

import { graphql } from '@/lib/gql'
import { useMutation } from 'urql'

const AddTodoMutation = graphql(/* GraphQL */ `
  mutation addTodoMutation($text: String!) {
    addTodo(text: $text) {
      id
    }
  }
`)

const formSchema = z.object({
  text: z.string().min(1),
})

export const AddTodoForm = () => {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      text: '',
    },
  })

  const [{ fetching }, addTodo] = useMutation(AddTodoMutation)

  const onSubmit = async (values: z.infer<typeof formSchema>) => {
    await addTodo(values)
    form.reset()
  }

  return (
    <Form {...form}>
      <form className={'flex space-x-4'} onSubmit={form.handleSubmit(onSubmit)}>
        <FormField
          control={form.control}
          name="text"
          render={({ field }) => (
            <FormItem className={'flex-1'}>
              <FormControl>
                <Input disabled={fetching} {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button disabled={fetching} type="submit">
          Add
        </Button>
      </form>
    </Form>
  )
}

Vercelにデプロイ

vercel

About