HapInS Developers Blog

HapInSが提供するエンジニアリングの情報サイト

犬無料10連ガチャを回していかないか?Ver.2【React/個人開発】

こちらはHapInSアドベントカレンダー2023、9日目の記事です!

はじめに

みなさまこんにちは!sawaです!

今回は去年Reactで作成した個人開発アプリにセルフダメ出しをアップデートしていこうと思います。

そのアプリがこちらの「犬無料10連ガチャ」です。

本記事を読んでいただくにあたり、少しだけ下記リンク先で動きを確認していただけるとわかりやすいかと思います! dog-gacha.netlify.app

そして下記リンクはこのアプリについて書いた去年のアドカレ記事です。

qiita.com

このちょっとしたアプリですが、ちょうどReactを学び始めたころに作成したので、今改めて見ると粗が目立ちます。

ですので今回のアドカレを機に改善していきたいと思います。 どうぞお付き合いください。

問題点

ざっと見て問題に感じた点は下記の通りです。

  • 1ファイルにコンポーネントが複数含まれている

  • ボタンの文言をuseEffectを使って切り替えている

  • APIをuseEffect内で叩くようになっている

  • リンク先が切れている画像が含まれている

順に修正していきましょう。

その前にひとつ前置きしておくと、コードを説明する際に不要な個所と前回の記事で説明してる個所については省いて記載しています。そちらを踏まえてご覧いただけますと幸いです。

【問題1】1ファイルにコンポーネントが複数含まれている

これについては特筆することはないです。コンポーネント毎にザクザク切り分けていきましょう。

時を経て改めて見ると構成を全く覚えていないので、このように1ファイルに1コンポーネントと切り分けていないと全体像が把握しにくいですね。実感しました。

【問題2】ボタンの文言をuseEffectを使って切り替えている

変更前

export const Top = () => {
  const [showDogImageFlag, setShowDogImageFlag] = useState(false);
  const onClickShowDogImage = () => {
    setShowDogImageFlag(!showDogImageFlag);
  };
  return (
    <STop>
      <div className="showResultButtonArea">
        //1回目に押すボタン
        <input
          type="button"
          value="回す!"
          onClick={onClickShowDogImage}
          className="showResultButton"
          showresult={showDogImageFlag ? "false" : "true"}
        />
      </div>
    </STop>
  );
};
export const DogCard = () => {
const [posts, setPosts] = useState([]);
const [renew, setRenew] = useState("");

  useEffect(() => {
    axios.get("https://dog.ceo/api/breeds/image/random/10").then((res) => {
      setPosts(res.data.message);
    });
 }, [setPosts, renew]);

  return (
    <SDogCard>
      <ul>
        {posts.map((post, i) => {
          return (
            <li key={i}>
              <img src={post} alt="犬の画像" loading="lazy" />
            </li>
          );
        })}
      </ul>
      //2回目以降に押すボタン
      <input
       type="button"
       value="もう一回!"
       onClick={() => setRenew((renew) => renew + 1)}
      />
    </SDogCard>
  );
};

主にこの辺りですね。

今見るとボタンを押すだけの処理にしては少し複雑になっていますね。 同じ役割を持つボタンであるのに、別々のコンポーネントに分けて置いてしまっていたり、別の関数を持たせているあたり保守性が低いです。

変更後

ここを下記のように変更しました。

まず前提として、ボタンを押す前は犬の画像を表示するエリアを非表示にしたいので、ボタンを1度押したかどうかをボタンの親コンポーネントでフラグ管理しています。

そのフラグをpropsとして子コンポーネントに渡し、フラグの真偽値によってボタンの文言を変えるようにしました。

これでよりシンプルな記述になったかと思います。

・親コンポーネント

export const Top = () => {
  const [showDogImageFlag, setShowDogImageFlag] = useState(false);
  const updateDogApiData = (data) => {
    setShowDogImageFlag(true);
  };

  return (
    <STop>
      <h1 className="title">犬無料10連ガチャ</h1>
      <div
        className="showResultArea"
        showresult={showDogImageFlag ? "false" : "true"}
      >
    <ResultArea dogApiData={dogApiData} />
      </div>
      <div className="showResultButtonArea">
        <FetchApiButton
          updateDogApiData={updateDogApiData}
          className="showResultButton"
          showDogImageFlag={showDogImageFlag}
        />
      </div>
    </STop>
  );
};

・子コンポーネント

export const FetchApiButton = (props) => {
  return (
    <SFetchApiButton>
      {props && props.showDogImageFlag ? (
        <input
          type="button"
          value="もう一回!"
          onClick={loadApi}//後述します
          className="showRenewButton"
        />
      ) : (
        <input
          type="button"
          value="回す!"
          onClick={loadApi}//後述します
          className="showRenewButton"
        />
      )}
    </SFetchApiButton>
  );
};

【問題3】APIをuseEffect内で叩くようになっている

変更前

ボタンを押したらsetRenewの値が変わり、その変化をuseEffectで拾っています。

const [posts, setPosts] = useState([]);
const [renew, setRenew] = useState("");

  useEffect(() => {
    axios.get("https://dog.ceo/api/breeds/image/random/10").then((res) => {
      setPosts(res.data.message);
    });
 [setPosts, renew]);

変更後

無駄に冗長になっているので、ボタンをクリックしたら直接APIを取得するように書き換えました。

そして本来はボタンコンポーネントAPIを取得するコンポーネントはそれぞれ切り分けるべきですが、めんどくさくなってひとつのコンポーネントにまとめて書いてしまっていますね……みなさんはちゃんと分けてください……

export const FetchApiButton = (props) => {
  const loadApi = () => {
    axios
      .get("https://dog.ceo/api/breeds/image/random/10")
      .then((res) => {
        props.updateDogApiData(res.data.message);
      })
      .catch((error) => {
        console.log(error);
      });
  };

  return (
    <SFetchApiButton>
      <input
        type="button"
        value="もう一回!"
        onClick={loadApi}
        className="showRenewButton"
      />
    </SFetchApiButton>
  );
};

【問題4】リンク先が切れている画像が含まれている

APIには画像のリンク先が切れているものも含まれています。

変更前

ちょうど画像のような状態になってしまいますね。

リンク切れしている画像

このあたりはどうしようもないので、リンク先が切れているものについては代替画像を用意して対処しましょう。

変更後

これはとても簡単に処理できます。

画像読み込み時のエラーを察知してくれる「onError」というイベントハンドラを使用しましょう。

関数内で代替画像のパスを渡してあげるだけでOKです。

<img
   src={post}
   alt="犬の画像"
   className="dogImg"
   loading="lazy"
   onError={(e) => {
       e.target.onError = null;
       e.target.src =
       "/assets/image/dog-gacha-images/ku01.jpg";
   }}
/>

React(Next.js)で画像がリンク切れの時に代替画像を表示するを参考にさせていただきました!

実際に画面で見てみましょう。

2件のリンク切れを代替画像に差し替えることができていますね。(青丸で囲ってある部分がリンク切れを起こしている個所です)

代替画像を表示している画像

トレーディングカード風にスタイルを変えていく

粗方問題に対処できたので、簡素だった画面を少しだけ華やかにしていきましょう!

トレーディングカード風の表現にしていきたいので、こちらのポケモンカードCSSで再現するテクニックを少し参考にさせていただきました。 deck-24abcd.netlify.app

ホログラムっぽい表現を演出

適当にそれっぽい画像を用意しました。

ホログラム用画像

まずこちらの画像を犬の画像に重ねます。

あとはCSSでグラデーションをかけてブレンドモードを使用して透過させれば完成です。

私は主に下記CSSのコードで実装しました。

background: linear-gradient(
      #d9fb69 20%,
      #fb6969 30%,
      #748bf9 48% 52%,
      #a5dc86 70%
    );
mix-blend-mode: color-dodge;
opacity: 0.3;

実際に見てみるとこんな感じですね。

ホログラム加工っぽくなりました。

ホログラム適用後画像

マウスストーカーで光を表現

簡単なマウスストーカーを作成して、光があたっているかのように演出します。

やり方としては、まずマウスの座標を検知して格納します。

そしてDOM要素にスタイルを動的に当ててあげることで簡単に実装しています。

const [isHover, setIsHover] = useState(false);
const [x, setX] = useState(0);
const [y, setY] = useState(0);
const mouseMove = (e) => {
    setX(e.clientX);
    setY(e.clientY);
};

<div
 className="image-container"
 onMouseMove={(e) => mouseMove(e)}
>
<div
className="highlight"
style={{ top: `${y - 30}px `, left: `${x - 30}px` }}
//-30pxにしているのは、このDOM要素の大きさが60px × 60pxなのでその半分を差し引いて中心が来るようにしているためです。
></div>

あとはホバーしたら画像を少し拡大してあげるアニメーションもつけました。

マウスストーカーするgif

ちょっとだけ華やかになったのではないでしょうか!

以上で時間切れとなったので、今年はここまで!

下記で挙動を確認してみてください。

leonlab.netlify.app

おわりに

Loadingのハンドリングができていないのと、スマホ対応が……とまだまだ直すところはあるのですが……また来年!

以上です!ここまで読んでいただきありがとうございました!

おまけ

読んでいただいたお礼にうちの犬を載せておきます!

かわいい犬

かわい~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ッッッッッッッッッッッッッッッッッッッッッッッッッッッッ!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!