Dalej bawimy się w pisanie prostej aplikacji quizowej, jeszcze będzie refactoring, ale na razie proste rzeczy, chcemy, aby nam działało, mieć funkcjonujący prototyp.

Ok, eslint narzekał, więc zmieniłem sposób w jaki eksportujemy pytania:

const QUESTIONS = [
    {
      id: 'q1',
      text: 'What is 2 + 2 = ?',
      answers: [
        '2',
        '4',
        '6',
        '8',
      ],
      correctIndex: 1
    },
    {
        id: 'q2',
        text: 'What is 2 x 2 = ?',
        answers: [
          '2',
          '4',
          '6',
          '8',
        ],
        correctIndex: 1
      },
      {
        id: 'q1',
        text: 'What is 2 + 2 x 2 = ?',
        answers: [
          '2',
          '4',
          '6',
          '8',
        ],
        correctIndex: 2
      },
  ];

  export default QUESTIONS;

App.js wygląda w ten sposób:

import './App.css';
import { useState} from "react";
import { Menu } from './Menu';
import { Quiz } from './Quiz';
import { Finish } from './Finish';

function App() {
  
  let [mode, setMode] = useState('menu');
  let [name, setName] = useState('');
  let [score, setScore] = useState(0);

  return (
    <div className="App">
        {mode === 'menu' && <Menu setMode={setMode} name={name} setName={setName}/> }
        {mode === 'quiz' && <Quiz name={name} setMode={setMode} score={score} setScore={setScore}/> }
        {mode === 'finish' && <Finish setMode={setMode} name={name} score={score} setScore={setScore} />}
    </div>
  )
  
}

export default App;

Rozrasta się, stany też się rozrastają i rzeczy przekazywane w dół, tutaj co najmniej trzeba będzie użyć reducera i kontekstu. Ok, lecimy do Quiz RFC:

import { Question } from './Question.js';

function SimpleSummary({idx, len}){
    return (
        <>
        <p>Question {idx + 1} / {len}</p>
        </>
    )
}


function Quiz({setMode, name, score, setScore}){

    const [userAnswers, setUserAnswers] = useState([]);
    const [timerKey, setTimerKey] = useState(1);

    const activeQuestionIndex = userAnswers.length;
    const quizIsComplete = activeQuestionIndex === QUESTIONS.length;

    function handleClick(selectedIdx){
            console.log(`Selected answer ${selectedIdx}`)
            if(selectedIdx === QUESTIONS[activeQuestionIndex].correctIndex){
                setScore(score => score + 1);
            }
            setUserAnswers((arr) => [...arr, selectedIdx]);
            setTimerKey((prev) => prev + 1);
    }
   
    return (
        <>
        <p>Your name: {name}</p>
        <p>Your score: {score}</p>
        {quizIsComplete || <Question timerKey={timerKey} index={activeQuestionIndex} handleAnswer={handleClick} /> }
        {quizIsComplete || <SimpleSummary idx={activeQuestionIndex} len={QUESTIONS.length}/>}
        {quizIsComplete && <button onClick={() => setMode('finish')}>Finish</button>}
       
        </>
    )
};

export {Quiz};

Też chyba raczej proste, btw to jest ten sam timer, który omawialiśmy 3 lekcje temu. Idziemy w dół do Question RFC:

import QUESTIONS from './questions.js';
import { Answers } from './Answers.js';
import Timer from './Timer.js';
function Question({index, handleAnswer, timerKey}){
    let timer = 5000;
    return (
        <>
        <Timer key={timerKey} timeout={timer} onTimeout={() => handleAnswer(null)}/>
        <h2>{QUESTIONS[index].text}</h2>
        <Answers answers={QUESTIONS[index].answers} handleAnswer={handleAnswer}/>
        <button onClick={() => handleAnswer(null)}>Skip Answer</button>
        </>
    )
};

export {Question};

Na razie ok, ale gdyby jakaś bardziej zaawansowana logika była, to nie dziwić się, że coś będzie wymagać useCallback. Funkcje to typy referencyjne i update stanu definiuje je od nowa z nową referencją.

Ok, to idziemy do Answers RFC:

function Answers({answers, handleAnswer}){
    return (
        <ul>
            {answers.map((ans, idx) => {
                return <li key={ans} onClick={() => handleAnswer(idx)} data-idx={idx}>{ans}</li>
            })}
        </ul>
    )
};

export {Answers};

Proste, aczkolwiek gdybyśmy chcieli np. dodać opcję mieszania odpowiedzi w różnych kolejnościach to będzie to wymagać pewnych zmian.

Teraz finish RFC:

function Finish({setMode, name, score, setScore}){
    function handleRestart(){
        setScore(0);
        setMode('menu');
    }
    return (
        <>
        <p>Quiz finished!</p>
        <p>Your name: {name}</p>
        <p>Your score: {score}</p>
        <button onClick={handleRestart}>Restart Quiz</button>
        </>
    )
};

export {Finish};

Jeszcze menu RFC, chciałbym zwrócić uwagę na 2 way data binding:

import { useRef, useEffect } from 'react';

function Menu({setMode, name, setName}){

    const btnRef = useRef(null);

    useEffect(() => {
        if(name === ""){
            btnRef.current.setAttribute("disabled", "");
        } else {
            btnRef.current.removeAttribute("disabled");
        }
      }, [name]);

    return (
        <>
        <p>Your name: {name}</p>
        <input type="text" onChange={(e) => setName(e.target.value)} value={name}/>
        <button ref={btnRef} onClick={() => setMode("quiz")}>Start Quiz</button>
        </>
    )
};

export {Menu};

Czyli dokładnie na tę porcję kodu:

<input type="text" onChange={(e) => setName(e.target.value)} value={name}/>

Jak nie ustawimy value na name to po restarcie zobaczymy stare imię w <p>, ale input będzie pusty.