2025年5月18日

React ViewTransitionを使ってみた

React ViewTransitionを使ってみた。

  • 手順1: React v19.1.0をインストール
    • npm install react@19.1.0 react-dom@19.1.0
  • 手順2: Next.jsを使っていたら、v15.2以上を利用
    • npm install next@15.2.0
  • 手順3: nextConfig.jsの修正
// nextConfig.js
const nextConfig = {
  experimental: {
    viewTransition: true,
  },
}

export default nextConfig
  • 手順4: コードを書く
'use client'

import {
  type ReactNode,
  unstable_ViewTransition as ViewTransition,
  startTransition,
  Suspense,
  useState,
} from 'react'

import Topltip from '@/_components/Tooltip'
import AnnotaionText from '@/_components/annotation/text'
import { CarouselCard } from '@/_features/blog/blog-carousel-card'
import { type BlogTitlesType } from '@/_features/blog/constant'

interface Carousel {
  title: string
  contents: BlogTitlesType
}

interface BlogCarouselProps {
  carousel: Carousel
}

export default function BlogCarousel({ carousel }: BlogCarouselProps) {
  const [orderedContents, setOrderedContents] = useState(carousel.contents)

  const reorder = () => {
    startTransition(() => {
      setOrderedContents((prev) => [...prev.sort(() => Math.random() - 0.5)])
    })
  }

  return (
    <>
      <Header
        title={carousel.title}
        action={
          <Topltip label="シャッフル">
            <button
              className="flex items-center rounded-full bg-gray-300 p-1 hover:bg-gray-400"
              onClick={reorder}
            >
              <span className="i-lucide-shuffle h-6 w-6 text-gray-800" />
            </button>
          </Topltip>
        }
      />

      <div className="flex gap-6 overflow-x-scroll">
        <Suspense fallback={<CarouselCard.Loading />}>
          {orderedContents.map((content) => (
            <ViewTransition
              key={content.year + content.month + content.date + content.title}
            >
              <CarouselCard {...content} />
            </ViewTransition>
          ))}
        </Suspense>
      </div>

      <div className="flex justify-center">
        <AnnotaionText label="※↑意図的に遅延読み込みをしています。" />
      </div>
    </>
  )
}

function Header({ title, action }: { title: string; action?: ReactNode }) {
  return (
    <div className="flex justify-between">
      <span className="i-lucide-arrow-left h-8 w-8 text-gray-300" />

      <div className="flex items-center gap-2">
        <div className="text-2xl font-bold text-gray-300">{title}</div>
        {action}
      </div>

      <span className="i-lucide-arrow-right h-8 w-8 text-gray-300" />
    </div>
  )
}