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?

More than 1 year has passed since last update.

React (Redux)の構成でアプリを作成しました【2】【簡単なクイズアプリ】

Last updated at Posted at 2021-10-03

1.コンポーネントを設定する

① create-react-app を使用して、セットアップする。

npx create-react-app <プロジェクト名>
cd <プロジェクト名>

② src / Components / Settings.js を作成する。

Settings.js
// src/Components/Settings.js
import React from 'react';

function Settings() {
	return (
		<div></div>
	);
}
export default Settings;

③ SettingsコンポーネントをApp.jsにインポートする。

App.js
// src/Components/Settings.js
import Settings from './Components/Settings';

import './App.css';

function App() {
  return (
    <div className="App">
      <h1>Quiz App</h1>
      <Settings />
    </div>
  );
}
export default App;

④ JSXを使用してインターフェースの構造を作成する。

APIの設定から、質問のカテゴリ、質問の難易度、質問のタイプ、質問の量をコントロールするための入力が必要で。

※ 質問カテゴリは、下記のエンドポイントから取得:
https://opentdb.com/api_category.php.
リクエストを行い、Reactのstate hookとeffect hookを使って、カテゴリーをコンポーネントに保存する。

Settings.js
// src/Components/Settings.js
import React, { useEffect, useState } from 'react';

function Settings() {
	// useState hook 
	const [options, setOptions] = useState(null);
	
	// useEffect hook
	useEffect(() => {
	    const apiUrl = `https://opentdb.com/api_category.php`;
	
	    fetch(apiUrl)
	      .then((res) => res.json())
	      .then((response) => {
	        setOptions(response.trivia_categories);
	      });
	  }, [setOptions]);

	return (
		<div></div>
	);
}

export default Settings;

⑤ reactからuseEffectとuseStateをインポートして、初期値をnullにしたオプション用のstate変数を宣言する。

ここには、setOptionsを使ってAPIレスポンスからのデータを格納します。
useEffectフックでは、APIのURLを宣言し、fetchを使ってリクエストを行い、setOptionsでペイロードとして必要なjsonを渡します。

Settings.js
// src/Components/Settings.js
import React, { useEffect, useState } from 'react';

function Settings() {
	const [options, setOptions] = useState(null);
	// add another useState hook
	const [questionCategory, setQuestionCategory] = useState("");

	useEffect(() => {
	    const apiUrl = `https://opentdb.com/api_category.php`;
	
	    fetch(apiUrl)
	      .then((res) => res.json())
	      .then((response) => {
	        setOptions(response.trivia_categories);
	      });
	  }, [setOptions]);

	// event that is called when an option is chosen
	const handleCategoryChange = event => {
    setQuestionCategory(event.target.value)
  }
	// add select elements for categories
	return (
		<div>
		<div>
          <h2>Select Category:</h2>
          <select value={questionCategory} onChange={handleCategoryChange}>
            <option>All</option>
            {options &&
              options.map((option) => (
                <option value={option.id} key={option.id}>
                  {option.name}
                </option>
              ))}
          </select>
        </div>
	</div>
	);
}
export default Settings;

⑥ questionCategoryの状態変数を宣言し、初期値を空の文字列にする。

※この値を更新するためにsetQuestionCategoryを使用する。

次に、全てのカテゴリから質問を取得するオプションを与えたいので、オプションとしてハードコードする。残りのオプションについては、オプションの配列をマッピングし、各オプションに反応性のためのキーを割り当てることを確認する。
※オプションの表示には名前を使用し、オプションの値にはAPIで期待される値であるidを使用する。

次に、handleCategoryChangeという名前のonChangeイベントを追加する。このイベントでは、ユーザーがオプションを選択するたびに、setQuestionCategoryを使用して、ステートquestionCategoryを更新する。

最後に、select要素の値をquestionCategoryにバインドする。

Settings.js
// src/Components/Settings.js
import React, { useEffect, useState } from 'react';

function Settings() {
	const [loading, setLoading] = useState(false);
	const [options, setOptions] = useState(null);
	const [questionCategory, setQuestionCategory] = useState("");

	useEffect(() => {
	    const apiUrl = `https://opentdb.com/api_category.php`;
	
	    setLoading(true);
	
	    fetch(apiUrl)
	      .then((res) => res.json())
	      .then((response) => {
					setLoading(false);
	        setOptions(response.trivia_categories);
	      });
	  }, [setOptions]);

	const handleCategoryChange = event => {
    setQuestionCategory(event.target.value)
  }

	if (!loading) {
		return (
			<div>
				<div>
	          <h2>Select Category:</h2>
	          <select value={questionCategory} onChange={handleCategoryChange}>
	            <option>All</option>
	            {options &&
              options.map((option) => (
                <option value={option.id} key={option.id}>
                  {option.name}
                </option>
              ))}
	          </select>
	        </div>
			</div>
		);
	} else {
		<p>
      LOADING...
    </p>
	}
}
export default Settings;

⑦ useEffectとuseStateパターンを使用して、src/Components/Settings.jsを下記のようにする。

残りの質問の選択肢は、APIエンドポイントでは利用できない為、ハードコーディングする必要がある。

Settings.js
// src/Components/Settings.js
import React, { useEffect, useState } from 'react';

function Settings() {
  const [loading, setLoading] = useState(false);
  const [options, setOptions] = useState(null);
  const [questionCategory, setQuestionCategory] = useState("");
  const [questionDifficulty, setQuestionDifficulty] = useState("");
  const [questionType, setQuestionType] = useState("");
	const [numberOfQuestions, setNumberOfQuestions] = useState(50);

  useEffect(() => {
    const apiUrl = `https://opentdb.com/api_category.php`;

    setLoading(true);

    fetch(apiUrl)
      .then((res) => res.json())
      .then((response) => {
        setLoading(false);
        setOptions(response.trivia_categories);
      });
  }, [setOptions]);

  const handleCategoryChange = event => {
    setQuestionCategory(event.target.value)
  }

  const handleDifficultyChange = event => {
    setQuestionDifficulty(event.target.value)
  }

  const handleTypeChange = event => {
    setQuestionType(event.target.value)
  }

	const handleAmountChange = event => {
    setNumberOfQuestions(event.target.value)
  }

  if (!loading) {
    return (
      <div>
        <div>
          <h2>Select Category:</h2>
          <select value={questionCategory} onChange={handleCategoryChange}>
            <option>All</option>
            {options &&
              options.map((option) => (
                <option value={option.id} key={option.id}>
                  {option.name}
                </option>
              ))}
          </select>
        </div>

        <div>
          <h2>Select Difficulty:</h2>
          <select value={questionDifficulty} onChange={handleDifficultyChange}>
            <option value="" key="difficulty-0">All</option>
            <option value="easy" key="difficulty-1">Easy</option>
            <option value="medium" key="difficulty-2">Medium</option>
            <option value="hard" key="difficulty-3">Hard</option>
          </select>
        </div>

        <div>
          <h2>Select Question Type:</h2>
          <select value={questionType} onChange={handleTypeChange}>
            <option value="" key="type-0">All</option>
            <option value="multiple" key="type-1">Multiple Choice</option>
            <option value="boolean" key="type-2">True/False</option>
          </select>
        </div>

				<div>
          <h2>Amount of Questions:</h2>
          <input value={numberOfQuestions} onChange={handleNumberOfQuestions} />
        </div>
      </div>
    );
  }

  return (
    <p>
      LOADING...
    </p>
  );
}
export default Settings;

2.シンプルなredux storeのセットアップ

① 以下を実行してreduxとreact-reduxをアプリに追加する。

yarn add redux react-redux

② App.jsを下記のように更新する。

App.js
// src/App.js
import App from './App';
import Reducer from './Reducer'
import { createStore } from 'redux';
import { Provider } from 'react-redux';

const store = createStore(Reducer);

ReactDOM.render(
  <React.StrictMode>
    <App />
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
);

③ src/Reducer.jsを作成し、オプションの状態をsrc/Components/Settings.jsからreduxの状態に移動する。

下図のように、初期状態を変数として宣言する。
この変数の options キーの下に、Settings コンポーネントで現在保持されている値を格納することができる。

Reducer.js
// src/Reducer.js
const initState = {
  options: {
    loading: false,
    question_category: ``,
    question_difficulty: ``,
    question_type: ``,
    amount_of_questions: 50
  }
}
export default Reducer

④ reducerでswitch関数を使って、コンポーネントから送られるアクションを消費する。

Reducer.js
// src/Reducer.js
const initState = {
  options: {
    loading: false,
    question_category: ``,
    question_difficulty: ``,
    question_type: ``,
    amount_of_questions: 50
  }
}

const Reducer = (state = initState, action) => {
  switch (action.type) {

    case "CHANGE_LOADING":
      return {
        ...state,
        options: {
          ...state.options,
          loading: action.value
        }
      }

    case "CHANGE_CATEGORY":
      return {
        ...state,
        options: {
          ...state.options,
          question_category: action.value
        }
      }

    case "CHANGE_DIFFICULTY":
      return {
        ...state,
        options: {
          ...state.options,
          question_difficulty: action.value
        }
      }

    case "CHANGE_TYPE":
      return {
        ...state,
        options: {
          ...state.options,
          question_type: action.value
        }
      }
    
    case "CHANGE_AMOUNT":
      return {
        ...state,
        options: {
          ...state.options,
          amount_of_questions: action.value
        }
      }

    default:
      return state
  }
}
export default Reducer

⑤ Settingsコンポーネントに戻り、stateとeffectのフックをreduxアクションで置き換える。

react-reduxからuseSelectorとuseDispatchをimportする必要がある。useSelectorでstateにアクセスし、useDispatchでstateを更新することができる。

Settings.js
// src/Components/Settings.js
import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux'

function Settings() {
  const [options, setOptions] = useState(null);
	// replace state hooks with useSelector
  const loading = useSelector(state => state.options.loading)

  const questionCategory = useSelector(state => state.options.question_category)
  const questionDifficulty = useSelector(state => state.options.question_difficulty)
  const questionType = useSelector(state => state.options.question_type)
  const questionAmount = useSelector(state => state.options.amount_of_questions)
	
	// defining to dispatch the actions
  const dispatch = useDispatch()

  useEffect(() => {
    const apiUrl = `https://opentdb.com/api_category.php`;

    const handleLoadingChange = value => {
      dispatch({
        type: 'CHANGE_LOADING',
        loading: value
      })
    }

    handleLoadingChange(true);

    fetch(apiUrl)
      .then((res) => res.json())
      .then((response) => {
        handleLoadingChange(false);
        setOptions(response.trivia_categories);
      });
  }, [setOptions, dispatch]);

	// replace setState with actions
  const handleCategoryChange = event => {
    dispatch({
      type: 'CHANGE_CATEGORY',
      value: event.target.value
    })
  }
  const handleDifficultyChange = event => {
    dispatch({
      type: 'CHANGE_DIFFICULTY',
      value: event.target.value
    })
  }
  const handleTypeChange = event => {
    dispatch({
      type: 'CHANGE_TYPE',
      value: event.target.value
    })
  }
  const handleAmountChange = event => {
    dispatch({
      type: 'CHANGE_AMOUNT',
      value: event.target.value
    })
  }
...

ここでは、JSXを一切変更していない。理由は、全ての変更イベントを関数できちんと処理しているからである。アクションを発信するフック関数を変更するだけで、アクションタイプと値をペイロードとして渡すことができる。

補足:handleCategoryChange内の変更

具体例
元々のソースコード

Originally.js
const handleCategoryChange = event => {
  setQuestionCategory(event.target.value)
}

変更後のソースコード

Andnow.js
const handleCategoryChange = event => {
  dispatch({
    type: 'CHANGE_CATEGORY',
    value: event.target.value
  })
}

payload objectの中でActionのタイプを定義する。
これは、このActionがReducer.jsのswitch関数によって解釈される為。

Reducer.js
// src/Components/Reducer.js
...
case "CHANGE_CATEGORY":
      return {
        ...state,
        options: {
          ...state.options,
          question_category: action.value
        }
      }
...

つまり、Reducerはこの場合、reduxの状態でquestion_categoryを更新したいと認識している。
payload内の値は、単純に、store内でquestion_categoryを更新したい値である。

また、useDispatch()が下記のように変数に割り当てられていることに気づくかもしれない。

const dispatch = useDispatch()

これは、依存関係としてuseEffect()に渡すことができるようにするためである。まだ、hookで質問カテゴリを取得しており、読み込み状態を更新するときにhook内からActionをdispatchする必要がある。

3.質問リクエストの作成

質問のオプションがredux storeに用意され、全てのコンポーネントがアクセスできるようになったので、クイズの質問のAPIリクエストを行い、storeに保存することができる。

① これを行うコンポーネントとして、FetchButtonを作成する。

FetchButton.js
// src/Components/FetchButton.js
import React from 'react';
import { useSelector } from 'react-redux';

function FetchButton(props) {
	// access the settings that will be used to construct the API query
  const questionCategory = useSelector(state => state.options.question_category)
  const questionDifficulty = useSelector(state => state.options.question_difficulty)
  const questionType = useSelector(state => state.options.question_type)
  const questionAmount = useSelector(state => state.options.amount_of_questions)
  const questionIndex = useSelector(state => state.index)
	
  const handleQuery = async () => {
		// we always need to specify the number of questions that we
		// want to be returned
    let apiUrl = `https://opentdb.com/api.php?amount=${questionAmount}`;

		// only add the rest of the parameters if they aren't 'all'
    if (questionCategory.length) {
      apiUrl = apiUrl.concat(`&category=${questionCategory}`)
    }

    if (questionDifficulty.length) {
      apiUrl = apiUrl.concat(`&difficulty=${questionDifficulty}`)
    }

    if (questionType.length) {
      apiUrl = apiUrl.concat(`&type=${questionType}`)
    }

    await fetch(apiUrl)
      .then((res) => res.json())
      .then((response) => {
        // this is where we will set questions in the state using an action
      });
  }
	// we will resuse this component, so the button text will be passed as props
  return <button onClick={handleQuery}>{props.text}</button>;
}
export default FetchButton;

ここでは、useSelector を使ってstoreから関連情報を収集する。次に、ボタンがクリックされると、handleQueryでこの情報を処理する。APIのURLパラメータは、オプションが選択されたかどうかによって動的に構築される。ユーザーがオプションを『all』にした場合は、パラメータとして追加されない。

② 同じ関数内で、fetchを使ってAPIリクエストを行う。

あとは、レスポンスを処理して、ステートに設定する。

FetchButton.js
// src/Components/FetchButton.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';

function FetchButton(props) {

  ...

  const dispatch = useDispatch()

  const setLoading = value => {
    dispatch({
      type: 'CHANGE_LOADING',
      loading: value
    })
  }

  const setQuestions = value => {
    dispatch({
      type: 'SET_QUESTIONS',
      questions: value
    })
  }

  const handleQuery = async () => {
    
		...
	
    setLoading(true);

    await fetch(apiUrl)
      .then((res) => res.json())
      .then((response) => {
        setQuestions(response.results)
        setLoading(false);
      });
  }

  ...

③ ここでは、新しいアクション「SET_QUESTION」を追加しているので、Reducer.jsのswitch関数も更新する。

Reducer.js
// src/Reducer.js
const initState = {
  options: {
    loading: false,
    question_category: ``,
    question_difficulty: ``,
    question_type: ``,
    amount_of_questions: 50
  },
  questions: []
}

const Reducer = (state = initState, action) => {
  switch (action.type) {

    ...

    case "SET_QUESTIONS":
      return {
        ...state,
        questions: action.questions
      }

    default:
      return state
  }
}

export default Reducer

4.質問コンポーネントの追加

質問がstateに保存されたので、質問を表示し、ユーザーが答えを選ぶためのコンポーネントを作成できる。ユーザーが現在取り組んでいる質問のインデックスと、ユーザーの現在のスコアを保存する必要があるので、これらを初期値0でredux storeに追加する。さらに、これらの値を更新するアクションのインスタンスを、Reducer switchステートメントに追加する。

Reducer.js
// src/Reducer.js
const initState = {
  ...
  index: 0,
  score: 0
}

const Reducer = (state = initState, action) => {
  switch (action.type) {

    ...

    case "SET_INDEX":
      return {
        ...state,
        index: action.index
      }
    
    case "SET_SCORE":
      return {
        ...state,
        score: action.score
      }

    default:
      return state
  }
}
export default Reducer

これらを、src/Components/Question.jsにある空のコンポーネントに、下記のように追加する。

Question.js
// src/Components/Question.js
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux'

function Question() {
	// retrieve score, questions and index from the store
  const score = useSelector(state => state.score)

  const questions = useSelector(state => state.questions)
  const questionIndex = useSelector(state => state.index)
	// define dispatch
  const dispatch = useDispatch()
	// create variables for the question and correct answer
  const question = questions[questionIndex]
  const answer = question.correct_answer

	return <div></div>
}
export default Question;

スコア、質問、質問のインデックスをreduxストアから取得するだけでなく、現在の質問と正解を表す変数があり、 useDispatchもインポート、定義されており、すぐに使用できる。

質問の形は以下の通りになる。

{
  "category": "Entertainment: Video Games",
  "type": "boolean",
  "difficulty": "easy",
  "question": "Peter Molyneux was the founder of Bullfrog Productions.",
  "correct_answer": "True",
  "incorrect_answers": [
    "False"
  ]
}

正解は、不正解の配列とは別に返される。そこで、正解と不正解の選択肢を一緒に返し、正解を配列のランダムな位置に配置するシンプルな useEffect 関数を書いた。そして、答えの選択肢は、オプションとして状態に保存される。

Question.js
// src/Components/Question.js
...
const [options, setOptions] = useState([])
...

const getRandomInt = max => {
  return Math.floor(Math.random() * Math.floor(max));
}

useEffect(() => {
  if (!question) {
    return;
  }
  let answers = [...question.incorrect_answers]
  answers.splice(getRandomInt(question.incorrect_answers.length), 0, question.correct_answer)

  setOptions(answers)
}, [question])

...

QuestionコンポーネントのJSXを追加する前に考慮すべき最後の点は、「未回答」と「回答済み」の状態が必要である。ユーザーが回答を選択する前に表示する必要のある主な情報は、質問と回答の選択肢である。ユーザーが答えを選択したら、正しい答えを選択したかどうかを表示し、選択していない場合は、正しい答えを表示する。これらをすべて同じコンポーネント内で行うので、この情報をreduxストアに送る必要はなく、フックを使ってこの状態を制御できる。

Question.js
// src/Components/Question.js
...

const [answerSelected, setAnswerSelected] = useState(false)
const [answerCorrect, setAnswerCorrect] = useState(null)

...

const handleListItemClick = event => {}

return (
  <div>
    <p>Question {questionIndex + 1}</p>
    <h3>{question.question}</h3>
    <ul>
      {options.map((option, i) => (
        <li key={i} onClick={handleListItemClick}>
          {option}
        </li>
      ))}
    </ul>
    <div>
      Score: {score} / {questions.length}
    </div>
  </div>
)

現時点では、このコンポーネントは、最初の質問と回答の選択肢を表示するが、選択肢をクリックしても何も更新されない。この問題を解決する為に、オプションのonClickハンドラに機能を追加する。クリックすると、次のことが必要になる。

  • answerSelectedをtrueに設定。
  • 選択された内容に応じて、answerCorrect を true または false に設定する。
  • 答えが正しければ、スコアを更新する。
  • 最終問題でない場合、インデックスを更新して次の問題に進む。
Question.js
// src/Components/Question.js
...

const handleListItemClick = event => {
  setAnswerSelected(true)
  setSelectedAnswer(event.target.textContent)

  if (event.target.textContent === answer) {
    dispatch({
      type: 'SET_SCORE',
      score: score + 1,
    })
  }

  if (questionIndex + 1 <= questions.length) {
    setTimeout(() => {
      setAnswerSelected(false)
      setSelectedAnswer(null)

      dispatch({
        type: 'SET_INDEX',
        index: questionIndex + 1,
      })
    }, 2500)
  }
}

...

パラメータに2500msを指定したsetTimeout関数は、ユーザーが答えを正解したかどうか、正解を1秒間表示してから次の問題に移ることを意味する。

次に、APIから送られてきたテキストをデコードする必要がある。
このためのネイティブなJavascriptの関数がないので、ここからソリューションを実装した。

...
const decodeHTML = function (html) {
  const txt = document.createElement('textarea')
  txt.innerHTML = html
  return txt.value
}

...

const encodedQuestions = useSelector((state) => state.questions)

  useEffect(() => {
    const decodedQuestions = encodedQuestions.map(q => {
      return {
        ...q,
        question: decodeHTML(q.question),
        correct_answer: decodeHTML(q.correct_answer),
        incorrect_answers: q.incorrect_answers.map(a => decodeHTML(a))
      }
    })

    setQuestions(decodedQuestions)
  }, [encodedQuestions])

...

最後に、ユーザーが選択した後の選択肢に、動的なクラスを追加します。if文を使って、
以下のようにした。

  • 答えがまだ選択されていない場合、動的なクラス名を持つリストアイテムはない。
  • 選択肢が選択されている場合、正しい答えには正しいクラスが設定される。
  • 選択肢が選択されている場合、ユーザーがクリックしたリストアイテムはselectedクラスを持つ。
const getClass = option => {
    if (!answerSelected) {
      return ``;
    }

    if (option === answer) {
      return `correct`
    }

    if (option === selectedAnswer) {
      return `selected`
    }
  }

...

<ul>
  {options.map((option, i) => (
    <li key={i} onClick={handleListItemClick} className={getClass(option)}>
      {option}
    </li>
  ))}
</ul>

...

App.cssでは、これらのクラスのスタイリングを追加することができる。リストアイテムのクラスが正しい場合、その背景色は緑色になる。リストアイテムが選択されたクラスを持ち、かつ正しいクラスを持っていない場合、その背景は赤になる。したがって、ユーザーが正しい項目を選択した場合は、緑色になる。不適切な項目を選択した場合は赤になり、正解は緑でハイライトされる。

...

li:hover {
  background-color: white;
  color: #222;
}

li.correct {
  background-color: rgb(53, 212, 53);
}

li.selected:not(correct) {
  background-color: rgb(206, 58, 58);
}

...

下記がQuestionコンポーネントの完成形。

Question.js
// src/Components/Question.js
import React, { useState, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'

const decodeHTML = function (html) {
  const txt = document.createElement('textarea')
  txt.innerHTML = html
  return txt.value
}

function Question() {
  const [questions, setQuestions] = useState([])
  const [answerSelected, setAnswerSelected] = useState(false)
  const [selectedAnswer, setSelectedAnswer] = useState(null)
  const [options, setOptions] = useState([])

  const score = useSelector((state) => state.score)
  const encodedQuestions = useSelector((state) => state.questions)

  useEffect(() => {
    const decodedQuestions = encodedQuestions.map(q => {
      return {
        ...q,
        question: decodeHTML(q.question),
        correct_answer: decodeHTML(q.correct_answer),
        incorrect_answers: q.incorrect_answers.map(a => decodeHTML(a))
      }
    })

    setQuestions(decodedQuestions)
  }, [encodedQuestions])
  const questionIndex = useSelector((state) => state.index)

  const dispatch = useDispatch()

  const question = questions[questionIndex]
  const answer = question && question.correct_answer

  const getRandomInt = (max) => {
    return Math.floor(Math.random() * Math.floor(max))
  }

  useEffect(() => {
    if (!question) {
      return;
    }
    let answers = [...question.incorrect_answers]
    answers.splice(getRandomInt(question.incorrect_answers.length), 0, question.correct_answer)

    setOptions(answers)
  }, [question])
const handleListItemClick = (event) => {
    setAnswerSelected(true)
    setSelectedAnswer(event.target.textContent)

    if (event.target.textContent === answer) {
      dispatch({
        type: 'SET_SCORE',
        score: score + 1,
      })
    }

    if (questionIndex + 1 <= questions.length) {
      setTimeout(() => {
        setAnswerSelected(false)
        setSelectedAnswer(null)

        dispatch({
          type: 'SET_INDEX',
          index: questionIndex + 1,
        })
      }, 2500)
    }
  }

  /*
    {
      "category": "Entertainment: Video Games",
      "type": "boolean",
      "difficulty": "easy",
      "question": "Peter Molyneux was the founder of Bullfrog Productions.",
      "correct_answer": "True",
      "incorrect_answers": [
        "False"
      ]
    }
  */
const getClass = option => {
    if (!answerSelected) {
      return ``;
    }

    if (option === answer) {
      return `correct`
    }

    if (option === selectedAnswer) {
      return `selected`
    }
  }

  if (!question) {
    return <div>Loading</div>
  }

  return (
    <div>
      <p>Question {questionIndex + 1}</p>
      <h3>{question.question}</h3>
      <ul>
        {options.map((option, i) => (
          <li key={i} onClick={handleListItemClick} className={getClass(option)}>
            {option}
          </li>
        ))}
      </ul>
      <div>
        Score: {score} / {questions.length}
      </div>
    </div>
  )
}
export default Question

5.最終ページ

FinalScreenは、クイズアプリのために作る最後のコンポーネントである。

現時点では、ユーザーが最後の質問に答えた後、どこにも行くことができず、
その答えが正解か不正解かを示すページに留まっている。

問題を終了する方法はいくつかあるが、ユーザーに3つの選択肢を与えることにする。

  • 同じ質問を再度試す
  • 同じ設定で新しい質問を取得する
  • 設定ページに戻る

ユーザーが同じ質問を再試行することを選択した場合、質問インデックスとスコアを0に戻すだけで、最初の質問に戻される。

FinalScreen.js
// src/Components/FinalScreen.js
...

const dispatch = useDispatch()

  const replay = () => {
    dispatch({
      type: 'SET_INDEX',
      index: 0
    })

    dispatch({
      type: 'SET_SCORE',
      score: 0
    })
  }
...

ユーザーが新しい質問を取得することを選択した場合、私たちはインデックス、スコアをリセットして、質問のために別のAPIコールを行う必要がある。これを行うには、設定ですでにインポートしているFetchButtonコンポーネントを再利用する。設定はreduxストアに保存されているため、ここではいくつかのことを行うだけで済む。

  • FetchButtonにpropとしてボタンテキストを提供する
  • questionIndexが0より大きい場合、FetchButtonのhandleQuery関数で質問インデックスとスコアをリセットする
    ※ これは、ユーザーがより多くの質問を要求することを選択した場合、最初の質問に行き、スコアをリセットしたいからである。

最後に、reduxストアに質問が存在しない場合、App.jsで設定コンポーネントが表示されるので、ユーザーが設定に戻ることを選択した場合、ストアの質問配列を空にして、スコアを0にリセットする。

FinalScreen.js
// src/Components/FinalScreen.js
...
const settings = () => {
    dispatch({
      type: 'SET_QUESTIONS',
      questions: []
    })

    dispatch({
      type: 'SET_SCORE',
      score: 0
    })
  }
...

FinalScreenコンポーネントの全貌

FinalScreen.js
// src/Components/FinalScreen.js
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'

import FetchButton from './FetchButton'

function FinalScreen() {
  const score = useSelector((state) => state.score)

  const dispatch = useDispatch()

  const replay = () => {
    dispatch({
      type: 'SET_INDEX',
      index: 0,
    })

    dispatch({
      type: 'SET_SCORE',
      score: 0,
    })
  }

  const settings = () => {
    dispatch({
      type: 'SET_QUESTIONS',
      questions: [],
    })

    dispatch({
      type: 'SET_SCORE',
      score: 0,
    })
  }

  return (
    <div>
      <h3>Final Score: {score}</h3>
      <button onClick={replay}>Try again</button>
      <FetchButton text="Fetch new questions" />
      <button onClick={settings}>Back to settings</button>
    </div>
  )
}
export default FinalScreen

参考サイト

Building a Simple Quiz App

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?