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.