Next.js(Reactフレームワーク)上でローディングアニメーションを実装した。しかし画面遷移のたびにローディングアニメーションが発動して鬱陶しい。
そこで、「1セッションに対してローディングアニメーションを1回だけ発動させる」実装をやってみた。
今回はそのときの備忘録を残していきます。
デモサイト
下記が制作したデモサイトです。
制御したいアニメーション
今回制御したいのは、ローディング時にファーストビューの視界が中央から左右に向かって広がるアニメーションです。
デモサイトでは制御の実装済みなので、ローディングが連発して鬱陶しいという状態は改善されています。
下記の記事にて、Reactコンポーネント上でローディングアニメーションを実装しております。
使用フレームワークとライブラリ
修正前ソースコード
"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で「最初の閲覧かどうかの状態管理」をしたいと思います。
"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回だけ発動させる」実装が完了しました。
今回、下記のフックを使用しています。
なぜ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なしでも実装できるけど楽をするために利用する」という感じになれるように繰り返し触っていきたいと思います。