ローディングアニメーションを連発させない【React】

React上でGSAP JavaScript

Next.js(Reactフレームワーク)上でローディングアニメーションを実装した。しかし画面遷移のたびにローディングアニメーションが発動して鬱陶しい。

そこで、「1セッションに対してローディングアニメーションを1回だけ発動させる」実装をやってみた。

  • 1度ローディングアニメーションが実行されたらセッションが途切れるまで再実行されないようにする。
  • ユーザーがタブを閉じるなどしてセッションが途切れたら、再実行されるようにする。

今回はそのときの備忘録を残していきます。

デモサイト

下記が制作したデモサイトです。

制御したいアニメーション

今回制御したいのは、ローディング時にファーストビューの視界が中央から左右に向かって広がるアニメーションです。

デモサイトでは制御の実装済みなので、ローディングが連発して鬱陶しいという状態は改善されています。

下記の記事にて、Reactコンポーネント上でローディングアニメーションを実装しております。

使用フレームワークとライブラリ

  • Next.js(バージョン14.2.25)
  • React.js(バージョン18)
  • GSAP(バージョン3.12.7)

修正前ソースコード

"use client";

import { useEffect, useRef } from "react";
import gsap from "gsap";
import styles from "./index.module.css";

export const Loader = () => {

    const leftRef = useRef<HTMLDivElement>(null);
    const rightRef = useRef<HTMLDivElement>(null);

    useEffect(() => {
            gsap.to([leftRef.current, rightRef.current], {
                duration: 1,
                width: "0%",
                ease: "power2.inOut",
                delay: 0.5,
            });
    }, []);

    return (
        <div className={styles.loader}>
            <div ref={leftRef} className={styles.left} />
            <div ref={rightRef} className={styles.right} />
        </div>
    );

};

上記のソースコードで無事にローディング時にアニメーションするようにはなりました。しかし画面遷移のたびにアニメーションが発動して鬱陶しい。

今回はその状態から修正を行い、1セッションに対してローディングアニメーションを1回だけ発動させるようにしていきます。

アニメーション発動を制御するには

「1訪問に対して1アニメーションのみを発動させる」ためには下記の3つが必要です。

  • 最初の閲覧かどうかの状態を管理する
  • セッション状況を踏まえて状態を変更する
  • 最初の閲覧だったらアニメーションを発動するよう制御

そこで今回は以下のフックを使用します。

  • useState
  • useEffect(2回使う)

最初の閲覧かどうかの状態を管理

まずは以下のことをやっていきます。

  • 最初の閲覧かどうかの状態を管理する

useStateで「最初の閲覧かどうかの状態管理」をしたいと思います。

"use client";
import { useEffect, useRef, useState } from "react";
import gsap from "gsap";
import styles from "./index.module.css";

useStateをインポートして状態管理ができるようにしておきます。

export const Loader = () => {

Loaderコンポーネントの中身を追記していきます。

const leftRef = useRef<HTMLDivElement>(null);
const rightRef = useRef<HTMLDivElement>(null);
const [showLoading, setShowLoading] = useState(false);

最初の閲覧かどうかを状態管理するためのステートを作成しました。

最初の閲覧だったらローディングアニメーションをユーザーに見せたいので、変数名は「showLoading」に、ステート名は「setShowLoading」にしています。

状況を踏まえて状態を変更

次は以下のことをやっていきます。

  • セッション状況を踏まえて状態を変更する

先ほど最初の閲覧かどうかを状態管理するために 「setShowLoading」というステートを作成しました。

ユーザーがサイトに訪問して最初の閲覧だったら、作成したステートが真(true)になるようにしていきます。

useEffect(() => {
        const hasVisited = sessionStorage.getItem("hasVisited");
        if (!hasVisited) {
            setShowLoading(true);
            sessionStorage.setItem("hasVisited", "true");
        }
    }, []);

ここで使うのが「sessionStorage」プロパティです。これを使うと、ユーザーが既にサイト訪問中だったかどうかの情報を取得できます。

これと似たプロパティにlocalStorageがあります。

セッションが途切れるまでの履歴にアクセスできるのが「sessionStorage」で、無期限に保持される履歴に対してアクセスできるのが「localStorage」です。

sessionStorageセッションが途切れるまでの履歴にアクセスできる
localStorage無期限に保持される履歴にアクセスできる

今回は「1セッションに対してローディングアニメーションを1回だけ発動させる」ことが目的なので、sessionStorageを使います。

const hasVisited = sessionStorage.getItem("hasVisited");

sessionStorageでセッション情報を取得し、それを変数hasVisitedに格納しました。

if (!hasVisited) {
            setShowLoading(true);
            sessionStorage.setItem("hasVisited", "true");
        }

変数hasVisitedが偽だったら、ステートをtrueにする。そして、hasVisitedを真にする。

これでユーザーがサイトに訪問して最初の閲覧だったら、作成したステートが真(true)になるようにできました。

最初の閲覧だったら発動

先ほど、セッション状況に応じてステートの値が変更されるようにしました。

最後に以下のことをやっていきます。

  • 最初の閲覧だったらアニメーションを発動するよう制御

変更されるようになったステートの値を利用し、「最初の閲覧だったらアニメーションを発動する」ようにしていきます。

もともと下記のような記述によって、gsapが実装されていました。

useEffect(() => {
            gsap.to([leftRef.current, rightRef.current], {
                duration: 1,
                width: "0%",
                ease: "power2.inOut",
                delay: 0.5,
            });
    }, []);

このままだとステートの値に応じて発動条件を制御することができていない。

そこで下記のように修正します。

    useEffect(() => {
        if (showLoading) {
            gsap.to([leftRef.current, rightRef.current], {
                duration: 1,
                width: "0%",
                ease: "power2.inOut",
                delay: 0.5,
            });
        }
    }, [showLoading]);

useEffectの第一引数で条件分を追加し、第二引数で発動条件を設定しました。

ステートの値(showLoading)が真のときだけ、gsapでのローディングアニメーションが発動するようになりました。

if (!showLoading) return null;

またステートの値(showLoading)が偽であればnullを出力するようにします。

これで「最初の閲覧だったらアニメーションを発動するよう制御」ができました。

使ったのは「useState」と「useEffect」

    return (
        <div className={styles.loader}>
            <div ref={leftRef} className={styles.left} />
            <div ref={rightRef} className={styles.right} />
        </div>
    );
};

今回はアニメーションの発動を制御しましたが、アニメーションそのものは変更なしなので、出力内容はそのままです。

以上で「1セッションに対してローディングアニメーションを1回だけ発動させる」実装が完了しました。

  • 最初の閲覧かどうかの状態を管理する
  • セッション状況を踏まえて状態を変更する
  • 最初の閲覧だったらアニメーションを発動するよう制御

今回、下記のフックを使用しています。

  • useState
  • useEffect(2回使う)

なぜuseEffectが必要なのか?

今回の実装でuseEffectを2回使いました。

useEffect(() => {
        const hasVisited = sessionStorage.getItem("hasVisited");
        if (!hasVisited) {
            setShowLoading(true);
            sessionStorage.setItem("hasVisited", "true");
        }
    }, []);
    useEffect(() => {
        if (showLoading) {
            gsap.to([leftRef.current, rightRef.current], {
                duration: 1,
                width: "0%",
                ease: "power2.inOut",
                delay: 0.5,
            });
        }
    }, [showLoading]);

useEffectを使わなかったらどうなるのか?これらは本当に必要なのか?

もしuseEffectを使わなかったら?

もしuseEffectを使わなかったら?

1つ目のuseEffect内ではsessionStorageプロパティというWebAPIを、2つ目のuseEffect内ではGSAPを使っています。

そのため、通常のレンダリングとは異なる「副作用」が発生する

そのまま処理させてしまうとコンポーネントの純粋性が保てません。それは避けたい。

またWebAPIもGSAPも外部システムを利用するものです。

なので、イベントハンドラで操縦することもできない

最終手段のuseEffect

  • 副作用が発生する
  • それでもコンポーネントの純粋性を保ちたい
  • 外部システムを利用しているからイベントハンドラでは無理

上記の状況を解決するために必要なのがuseEffectなのです。

useEffectのおかげで、sessionStorageプロパティもGSAPもコンポーネント内で使えるのです。

修正後ソースコード

下記が修正後のソースコードです。

"use client";

import { useEffect, useRef, useState } from "react";
import gsap from "gsap";
import styles from "./index.module.css";

export const Loader = () => {
    const leftRef = useRef<HTMLDivElement>(null);
    const rightRef = useRef<HTMLDivElement>(null);
    const [showLoading, setShowLoading] = useState(false);

    useEffect(() => {
        const hasVisited = sessionStorage.getItem("hasVisited");
        if (!hasVisited) {
            setShowLoading(true);
            sessionStorage.setItem("hasVisited", "true");
        }
    }, []);

    useEffect(() => {
        if (showLoading) {
            gsap.to([leftRef.current, rightRef.current], {
                duration: 1,
                width: "0%",
                ease: "power2.inOut",
                delay: 0.5,
            });
        }
    }, [showLoading]);

    if (!showLoading) return null;

    return (
        <div className={styles.loader}>
            <div ref={leftRef} className={styles.left} />
            <div ref={rightRef} className={styles.right} />
        </div>
    );
};

まとめ

「画面遷移のたびにアニメーションが発動して鬱陶しい状態」を脱するべく、発動条件の制御を実装してみました。

実装してみたとは言ったものの、正直なところ生成AIに手伝ってもらって実装しています。後から生成AIに質問を投げながら理解を深めていきました。

ReactのこともNext.jsのことも理解を深め、「生成AIなしでも実装できるけど楽をするために利用する」という感じになれるように繰り返し触っていきたいと思います。

タイトルとURLをコピーしました