💭

Chrome 111 新機能「 View Transitions API 」をReactで実装する

2023/03/11に公開
3

View Transitions API ってなに

Chrome111以降に導入された、DOM変更に伴うアニメーション機能です。
非常に簡単なJSとCSSで滑らかなトランジションを実現することができます。

View Transitions APIには、デフォルトでクロスフェードが実装されており、CSSを使用しなくてもAPIを試すことができます。

Reactで書いてみよう

2023年3月中旬の時点で、JavaScriptで書かれた記事しか見つからなかったため、React+Typescriptで実装する方法を模索しました。
「もっとこうしたらいい..!!」「ここがおかしいよ」など、お気づきの点はコメント大歓迎です。

成果物

スクリーンショット

View Transitions API対応の場合(Chrome111)
画像を切り替える際、クロスフェードになっています。

View Transitions API未対応の場合(Safari)
View Transitions API未対応でも、切り替えは行えます。

Reactデモサイト

無料枠なので若干カクカクするかもしれません。
https://react-transitions-api-demo-bzcgkrbdg-nyamadamadamada.vercel.app/

GitHub

https://github.com/Nyamadamadamada/React_Transitions_API

コード全体

App.tsx
App.tsx
import { useState } from "react";
const cdnURL = "https://react-transitions-api-demo.vercel.app/image/";

type Image = {
  name: string;
  file: string;
};

interface ExDocument extends Document {
  startViewTransition?: any;
}

const imageData: Image[] = [
  {
    name: "チューリップ",
    file: "25603452.png",
  },
  {
    name: "スカイツリー",
    file: "25447899.png",
  },
  {
    name: "つくし",
    file: "25794366.png",
  },
  {
    name: "ネモフィラ",
    file: "25839343.png",
  },
];

function App() {
  const [src, setSrc] = useState(cdnURL + imageData[0].file);
  const [text, setText] = useState(imageData[0].name);

  const handelClick = (data: Image) => {
    const displayNewImage = () => {
      setSrc(cdnURL + data.file);
      setText(data.name);
    };
    const doc: ExDocument = document;
    // View Transitions API未対応のブラウザの場合
    if (!doc.startViewTransition) {
      displayNewImage();
      return;
    }
    // 引数にDOM更新用のコールバック関数を渡す
    doc.startViewTransition(() => displayNewImage());
  };
  return (
    <div className="App">
      <h1>React View Transitions API demo</h1>
      <main>
        <section className="thumbs">
          {imageData.map((data) => {
            return (
              <a
                key={data.name}
                href="#"
                title={`Click to load ${data.name} in main gallery view`}
                onClick={() => handelClick(data)}
              >
                <img
                  alt={data.name}
                  src={cdnURL + "thumb/" + data.file}
                  width={100}
                  height={100}
                />
              </a>
            );
          })}
        </section>
        <section className="gallery-view">
          <figure>
            <img src={src} />
            <figcaption>
              <div className="caption-text">{text}</div>
            </figcaption>
          </figure>
        </section>
      </main>
      <footer className="footer">
        <a
          href="https://github.com/Nyamadamadamada/React_Transitions_API"
          target="_blank"
          rel="noopener noreferrer"
        >
          See source code
        </a>
      </footer>
    </div>
  );
}

export default App;

サムネ画像をクリックしたらメイン画像に差し替える

驚くべきことに、たった一行追加するだけでView Transitions APIを使えます!

document.startViewTransition(() => {/* DOM変更Function */});

クリック時、クリックした画像の名前とファイル名を渡し、値を更新しています。

  const handelClick = (data: Image) => {
    const displayNewImage = () => {
      setSrc(cdnURL + data.file);
      setText(data.name);
    };
    const doc: ExDocument = document;
    doc.startViewTransition(() => displayNewImage());
  };

View Transitions API未対応のブラウザの対応として、startViewTransitionを使わずにdisplayNewImage()を直接呼び出します。

// View Transitions API未対応のブラウザの場合
if (!doc.startViewTransition) {
  displayNewImage();
  return;
}

startViewTransitionをそのまま使うと型エラーが発生

TypeScriptのdocumentの中にstartViewTransitionがまだ含まれていないらしく、そのまま使用すると以下のエラーになります。

// Property 'startViewTransition' does not exist on type 'Document'.ts(2339)
document.startViewTransition(() => displayNewImage());

対策として、Documentを拡張し、startViewTransitionをany型にしました。
未対応ブラウザが存在するためundefind許可しています。

interface ExDocument extends Document {
  startViewTransition?: any;
}
const doc: ExDocument = document;
// 引数にDOM更新用のコールバック関数を渡す
doc.startViewTransition(() => displayNewImage());

追記 型エラー解消の方法

@sprout2000 さまよりコメントいただき、グローバル空間で型宣言する方法を教えていただきました!

ViewTransitionの型をDocumentに継承

global.d.ts
export interface ViewTransition {
  ready: Promise<void>;
  finished: Promise<void>;
  updateCallbackDone: Promise<void>;
  skipTransition: () => undefined;
}

declare global {
  interface Document {
    startViewTransition?: (skipTransition) => ViewTransition;
  }
}

TypeScript設定ファイルにグローバル型定義を追加

tsconfig.json
{
   "include": ["src","global.d.ts"],
}

documentのまま使える!!

App.tsx
  const handelClick = (data: Image) => {
    const displayNewImage = () => {
      setSrc(cdnURL + data.file);
      setText(data.name);
    };
    // View Transitions API未対応のブラウザの場合
    if (!document.startViewTransition) {
      displayNewImage();
      return;
    }
    // 引数にDOM更新用のコールバック関数を渡す
    document.startViewTransition(() => displayNewImage());
  };

@sprout2000 さまコメントありがとうございました!

最後に

簡単にかっこいいアニメーションが作れるのは最高にクールですね!
まだ対応していないブラウザがありますが、どんどん使っていきたいです。

今回は試していませんが、useRefを使って、特定の要素だけトランジションをかけたり、CSSアニメーションゴリゴリ書いても面白そうだなと思いました。
きっと誰かがやってくれるハズ!

参考

https://zenn.dev/yhatt/articles/cfa6c78fabc8fa

https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition

https://developer.chrome.com/docs/web-platform/view-transitions/

https://stackdiary.com/view-transitions-api/

Discussion

はっぱはっぱ

とても有用な記事をありがとうございます。

anyはあまり使いたくないのですが、コールバック関数の型がわからず。。。
ご存知の方がいたら、教えていただきたいです🙏

ご存知ではないのですが MDN 見ながら雰囲気で書いたらなんか動いちゃってますね...
たぶん間違ってると思いますが...

Global.d.ts
export interface ViewTransition {
  ready: Promise<void>;
  finished: Promise<void>;
  updateCallbackDone: Promise<void>;
  skipTransition: () => undefined;
}

export declare global {
  interface Document {
    startViewTransition?: (skipTransition) => ViewTransition;
  }
}
App.tsx
  const handelClick = (data: Image) => {
    const displayNewImage = () => {
      setSrc(cdnURL + data.file);
      setText(data.name);
    };

    if (!document.startViewTransition) {
      displayNewImage();
      return;
    }

    document.startViewTransition(() => displayNewImage());
  };
かのかのかのかの

コメントいただきありがとうございます!!
かっこいい型使いでトランジションできて感動です✨
内容を記事に追記させていただきましたm(_ _)m

はっぱはっぱ

やはり Vite.js 環境のおかげでたまたま動いていただけのようです。

スクショは parcel 環境

結局、コールバックの戻り値までは推測できないためこうなりました:

declare global {
  interface Document {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    startViewTransition?: (callback: () => any) => ViewTransition;
  }
}

ただ、React で使うのであれば、結局は setState メソッドをラップすることになるので void に決め打ちしても良いかもしれませんね。

declare global {
  interface Document {
    startViewTransition?: (callback: () => void) => ViewTransition;
  }
}