N-LAB

Next.js 13 × Client Components × SWR × Route Handlersでクライアントサイドからデータを取得する

目標


条件

・Next.js: 13.5.1
・Swr: 2.2.2
・Axios: 1.5.0
・Zod: 3.21.4

目次

  1. Client Components
  2. SWR
  3. Route Handlers
  4. 実装


Client Components


  1. クライアントサイドでのレンダリング(Javascriptはブラウザで実行)
  2. データのフェッチ(async/awaitの代わりにuseState・useEffect・SWR・React-queryを使用)
  3. イベントリスナーの追加(onClick()、onChange()など)
  4. Stateとライフサイクルエフェクトの使用 (useState(), useReducer(), useEffect() など)
  5. ブラウザ専用のAPIの使用
  6. State、エフェクト、またはブラウザ専用APIに依存するカスタムフックの使用
  7. Reactクラスのコンポーネントの使用


// app/counter/page.tsx.
'use client'

import { useEffect, useState } from 'react'

export default function Counter() {
  // useStateを利用してクリックした回数を保持
  const [count, setCount] = useState(0)

  // countの値が変わるたびに実行
  useEffect(() => {
    console.log('count', count)
  }, [count])

  // ボタンを押下するたびに実行
  const handleClick = () => {
    setCount(count + 1)
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={handleClick}>Click</button>
    </div>
  )
}


SWR

  https://swr.vercel.app/ja

Route Handlers

  https://nextjs.org/docs/app/building-your-application/routing/route-handlers

実装


$ npm install swr axios


app
├── routeHandlers
│     └── page.tsx ← クライアントで実行
└── api
     └── routeHandlers
          └── route.ts ← サーバーで実行


'use client'

import axios, { AxiosResponse } from 'axios'
import useSWR from 'swr'
import { useState } from 'react'

type TestResponse = {
  userId: string
  id: string
  title: string
  body: string
}

type TestRequest = {
  url: string
  postId: string
}

const fetcher = async (request: TestRequest) =>
  await axios
    .post(request.url, { postId: request.postId })
    .then((res: AxiosResponse<TestResponse>) => res.data)

export default function RouteHandlers() {
  const [postId, setPostId] = useState('1')

  const { data, error, isLoading } = useSWR<TestResponse, Error>(
    ['/api/routeHandlers', postId],
    ([url, postId]: [url: string, postId: string]) => fetcher({ url, postId }),
  )

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    const targetElement = e.currentTarget?.postId as HTMLInputElement
    if (targetElement) {
      setPostId(targetElement.value)
    }
  }

  if (error) {
    return <div>Failed to load</div>
  }

  if (isLoading) {
    return <div>Loading...</div>
  }

  return (
    <div>
      <div>
        <form onSubmit={handleSubmit}>
          <input name='postId' type='number' required placeholder='enter postId' />
          <button>search</button>
        </form>
      </div>
      <div>
        {data && Object.keys(data).length !== 0 ? (
          <div>
            <div key={data.id}>
              <h3>{data.title}</h3>
              <p title={data.body}>{data.body}</p>
            </div>
          </div>
        ) : (
          <h2>Not found</h2>
        )}
      </div>
    </div>
  )
}


import 'server-only'
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'

const userSchema = z.object({
  postId: z.string(),
})

type TestResponse = {
  userId: string
  id: string
  title: string
  body: string
}

export const POST = async (request: NextRequest) => {
  try {
    const result = userSchema.safeParse(await request.json())
    if (!result.success) {
      return NextResponse.json({}, { status: 400 })
    }

    const postId = result.data.postId.trim()
    const post = (await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`).then((res) =>
      res.json(),
    )) as TestResponse
    if (!post || Object.keys(post).length === 0) {
      return NextResponse.json({}, { status: 500 })
    }

    return NextResponse.json(post, { status: 200 })
  } catch (error) {
    return NextResponse.json({}, { status: 500 })
  }
}


以上で全ての手順は完了になります