0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【個人開発】誰でも100点取れて自己肯定感を爆上げできるクイズゲーム

Last updated at Posted at 2025-03-14

はじめに

この度、「誰でも100点が取れる(全問正解できる)」というコンセプトのクイズゲームをReact 19, TypeScript, viteで作りました。デプロイ先はVercelです。

皆様におきましては、常に全問正答して俺TUEEE(おれつえええ)という無双状態を体感し、自己肯定感を爆上げしてください。

ゲームの特徴

先に述べたように、 誰でも100点が取れる(全問正解できる) をコンセプトにしています。

具体的には以下キャプチャのように、
間違った回答を選ぶと強制的に選択不可となり、
回答が未チェックでは次の問題へは進めないため、
必然的に正答しか選べない状況になるのです。

dareore-usecap.gif

絶対に、間違いなく、100点を取れる、勝利が約束されたクイズゲームなので、自己肯定感を爆上げしてください。

無事に全問正答すると「褒められ画面」に移ります。

resultpage.png

余談ながら、上記の「褒め文章」や「各クイズの内容」は全て生成AIに用意してもらいました。
(だって、いちいち全部自分で用意してられないでしょ)

ChatGPT, Claude, Geminiの三方にお願いしましたが、一番Geminiがノリノリで作ってくれました。

用途

用途としては以下を想定しています。

  • 脳トレ(認知症の予防とか)
    間違った回答を選択した時点でヒントが表示されるので振り返り + 脳への刺激になる(かも?)

  • 学習の振り返り
    語句や専門用語、構文などのチェックといった学習面のほか、定型的な業務フローの確認(新人教育)など業務面といった振り返りに活用できる(かも?)

  • 子どもの知育
    表示された画像を含めて楽しく学べる(かも?)

  • 一般的なクイズ
    回答強制機能をオフにして一般的なクイズとして遊ぶこともできます。


現状、以下のカテゴリを準備しています。

  • 一般教養
  • 色々な動物たち
  • 子どもの知育
  • 地理・自然
  • 歴史(主に日本について)
  • ライフハック
  • IT・コンピューター全般
  • webサイト制作関連
  • ビジネス・社会(※回答強制なし)

例:「色々な動物たち」カテゴリーのキャプチャ

dareore-img.png


「勝ちゲーなんて、馬鹿にしてるのか!」という方向けに「ビジネス・社会」カテゴリは回答強制機能をオフにしています。

ちなみに、回答強制のオフはシンプルにクイズデータ(jsonファイル)の当該設問にadjustを用意しなければいいだけです。

「コード修正してビルドして~」などせずとも当該クイズデータ(jsonファイル)を修正するだけで済みます。

  • クイズデータ(jsonファイル)の一部
    • choices内の(各選択肢における)pointが100のものが正解です
    • 間違いの回答にadjustを付けると回答強制機能がオンになります。単体・複数キーワードどちらでもokで、複数キーワードの場合はランダム表示となります。
    • imgsrcには参照画像も設定可能です
{
    "quiz": "砂漠に住んでいて、コブが特徴的な動物は?",
    "imgsrc": "",
    "choices": {
        "one": {
            "txt": "カバ",
            "point": "0",
            "adjust": [
                "カバは水辺が好きだよね。",
                "コブはないし、砂漠には住まないよ。"
            ]
        },
        "two": {
            "txt": "ラクダ",
            "point": "100"
        },
        "three": {
            "txt": "ゾウ",
            "point": "10",
            "adjust": "ゾウも大きいけど、コブはないよね。"
        }
    }
},
{ ...
  • ロゴについて
    ロゴは生成AIではなく自作です。
    「誰でも100点が取れて俺TUEEE(おれつえええ)」を漢字四文字にしたイメージです。

『最近イラレ触ってないなぁ~』と思いまして、アピアランスの使い方とか忘れかけてたので定期的に触るのは大事ですね。

ところで、何でこんなん作ったん?

ただReact 19を試してみたかった、だけです。
そこで、以前React 18で作っていた個人開発をベースに今回作り変えました。

useAPI

特に、事前告知の段階からuseAPIに関して気になっていたので今回クイズのデータフェッチ処理部分で試せて良かったです。

こんな感じです。

// ...中略
export const FetchDataAndLoading = memo(() => {
    // 選んだクイズデータ
    const dynamicFetchPathUrl: string = `${selectQuiz.length !== 0 ? selectQuiz : selectQuizDefaultValue}/quiz.json`;

    // データフェッチするためのパス文字列
    const fetchPathUrl: string = `${import.meta.env.VITE_FETCH_URL}/jsons/quiz/${dynamicFetchPathUrl}`;

    // use APIを使うための下準備
    const fetchdataPromise: Promise<quizType[]> = fetch(fetchPathUrl).then(res => res.json());

    return (
        <>
            {selectQuiz.length > 0 &&
                <QuizComponent>
                    {/* Suspense 必須。fetchdataPromise が未完了なら */}
                    {/* Suspense の fallback が返される */}
                    <Suspense fallback={<Loading />}>
                        <QuizProgressBar fetchdataPromise={fetchdataPromise} />
                        <QuizContents fetchdataPromise={fetchdataPromise} />
                        <QuizBtn fetchdataPromise={fetchdataPromise} />
                        <ViewAnswers fetchdataPromise={fetchdataPromise} />
                    </Suspense>
                </QuizComponent>
            }
        </>
    );
    // ...中略

上記で用意したfetchdataPromise(クイズゲームの中身データ)を使用する各種コンポーネントで以下のように呼び出して使っています。

export const QuizProgressBar = memo(({ fetchdataPromise }: { fetchdataPromise: Promise<quizType[]> }) => {
    // fetchdataPromiseを取得
    const getData: quizType[] = use(fetchdataPromise);

    const { questionCounter } = useContext(QuestionCounterContext);

    return (
        <>
            {questionCounter < getData.length &&
                /* 設問数分の進行バー・ポイントを生成 */
                <ProgressBar id="progressBar">
                    <ul>
                        {getData.map((_, i) => (
                        // 中略...

いままでuseEffectとかを用いて行っていたフェッチ処理がとてもシンプルに行えるようになっていて、React 19では開発者体験がさらに良くなっていると感じました!

ありがとう! React

ところで、fetchPathUrlという変数にデータフェッチするためのパス文字列を入れています。
これは、publicjsonsというフォルダを設けて、そこでクイズや回答に関するコンテンツデータを一元管理しているためです。

こうすることで疑似的なCMSのように、jsons内のクイズや回答に関するファイルを編集または新規追加するだけでクイズゲームのコンテンツデータを柔軟に取り回せます。

新しいコンテクストプロバイダ

あと今回、新しいContextの書き方も試したかったのです。

// ...中略
export const QuestionCounterContextFlagment = (props: defaultContext) => {
    /* 設問数のカウンター */
    const [questionCounter, setQuestionCounter] = useState<number>(0);

    return (
        // React 19 以前の書き方では QuestionCounterContext.Provider 
        <QuestionCounterContext value={
            {questionCounter, setQuestionCounter}
        }>
            {props.children}
        </QuestionCounterContext>
    );
}

useActionState

完全に理解したわけではないですが使ってみて何となくは分かった(ような気がします)。

使うまでは旧称のuseFormStateという名称と公式ドキュメントの以下サンプルコード(formActionの部分)に引きずられて form処理関連でしか使わないもの という印象でした。

const [state, formAction, isPending] = useActionState(fn, initialState, permalink?);

しかし、実際に使ってみると非同期処理をシンプルに実装できる汎用性の高いフックという印象に変わりました。

筆者は今回、クイズゲームの結果表示に関するデータフェッチ処理で使用しました。

const [, fetchAnswersDataAction, isPending] = useActionState(
    async () => {
        // ...中略

        /* urlPathPart: フェッチする回答結果JSONファイルのパス名 */
        /* 回答強制機能がオフの場合に得点数に応じて回答結果が変わる */
        let urlPathPart: string = 'answer-low.json';
        if (scorePointRef.current >= highScorePoint) {
            urlPathPart = 'answer-high.json';
        } else if (scorePointRef.current >= mediumScorePoint && scorePointRef.current < highScorePoint) {
            urlPathPart = 'answer-medium.json';
        }

        // フェッチ処理
        const fetchUrlPath: string = `${import.meta.env.VITE_FETCH_URL}/jsons/answers/${selectQuiz}/${urlPathPart}`;
        const res: Response = await fetch(fetchUrlPath);
        const resObj: answerResultType[] = await res.json();

        setFetchAnswersData(resObj);

        return null;  // return newState;
    },
    null // initialState
);

const withTransition_fetchAnswersDataAction: () => void = () => {
    // startTransition がないと( action または Context の外部で dispatch したと)怒られる
    startTransition(() => {
        fetchAnswersDataAction();
    });
}

筆者はフェッチデータをsetFetchAnswersDataで状態管理しているので、今回のuseActionStateでは初期値はnullで、返却値もnullにしています。
加えて、返り値のstate部分も指定していません(※返却値がnullなため)

正直ここら辺はuseActionStateを完全に理解し切れておらず、もっと適切な実装があるように感じます。

今回使ってみて一つ興味深かったことは、先のコード内にも記載した startTransitionがないと(actionまたはContextの外部でdispatchしたと)怒られる という部分についてです。

当該コードの部分は以下のようにstartTransition無しでも問題なく機能しました。

const withTransition_fetchAnswersDataAction: () => void = () => {
    // startTransition がないと( action または Context の外部で dispatch したと)怒られる
-    startTransition(() => {
        fetchAnswersDataAction();
-    });
}

しかし、この場合は下記のようなエラーがログに出力されます。

An async function was passed to useActionState, but it was dispatched outside of an action context. This is likely not what you intended. Either pass the dispatch function to an action prop, or dispatch manually inside startTransition

useActionStateに非同期関数が渡されましたが、アクション・コンテキストの外でディスパッチされました。これはおそらく意図したものではありません。ディスパッチ関数を action プロップに渡すか、startTransition 内で手動でディスパッチしてください。

というわけで、useActionStateのアクション実行時にはstartTransitionを用いた方が無難だと思います。

理由は以下のページ情報が詳しいです。

ここでstartTransitionを使う理由は、useActionStateが返した関数はトランジションの内部で呼ばなければならないからです。このようにuseActionStateが作ったアクション(今回はincrement)を使う際は、まずトランジションを開始して、その中でアクションを呼び出しましょう。

ちなみに、筆者はアクション(withTransition_fetchAnswersDataAction)を、以下のように別のコンポーネントに渡して使っています。

<QuizBtn props={{
    getData: getData,
    scorePointRef: scorePointRef,
    withTransition_fetchAnswersDataAction: withTransition_fetchAnswersDataAction
}} />

// `button`の`onClick`イベントに withTransition_fetchAnswersDataAction を使用

このように子コンポーネントにuseActionStateで作ったアクションを渡して利用できるので柔軟性や汎用性も高いと感じました。

わがままをいえばカスタムフックのように、useActionStateで作ったステートやアクションだけを切り出して(exportできて) 自由に使えるともっと良いなとも感じましたが。


useAPI やuseActionStateしかり、React 19では非同期処理の実装がシンプルになっているように感じます。
この辺りはドキュメントや各種情報を追うよりも(筆者のように体で覚えるタイプは)自分で一度使ってみた方が分かりやすいかもしれません。

デプロイ(ホスティング)先

今回、Vercelを選びました。使い慣れているのが主な理由ですが、今度は気になっているCloudflare Pagesを使ってみたいなぁと思ってます。

ちなみに、VercelにデプロイしたのでデータフェッチのURLパスは環境変数に設定していますが、
国内ホスティングサービスなど環境変数の設定に対応していないホスティング先のケースにも対応できるようにしています。

これに関して気になる方は、当クイズゲームのリポジトリの README内にある環境変数に対応していないホスティング先の場合 を見ていただけますと幸いです。

さいごに

ここまで読んでいただき、ありがとうございます。

社会も、世界も、暗い雰囲気で、
学生も、社会人も、褒められる機会が減っていると思うので、
このクイズゲームで是非、自己肯定感を爆上げしてください(こじつけ)

当クイズゲームは自由に使っていただいて結構です。
GitHubを置いておきますので、関心のある方はどうぞ。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?