Powoli już nam się zaczyna układać ta aplikacja quizowa w biblioteczce React. Kontynuacja lekcji poprzednich, do dzieła.
Ok, oto nasz RFC App:
import './App.css';
import { QuizProvider } from './QuizContext';
import { Mainloop } from './Mainloop';
function App() {
return (
<QuizProvider>
<Mainloop/>
</QuizProvider>
)
}
export default App;
Ten mainloop to nie są jakieś jaja, on polega na kontekście z quiz providera. App nie może korzystać z tego kontekstu, dopiero dzieci app tak (chyba dałoby się go wrzucić w to miejsce, gdzie mamy React.StrictMode, ale mniejsza, to nie miejsce na to).
Ok, to idziemy do mainloop:
import { useQuiz } from "./QuizContext";
import { Menu } from "./Menu";
import { Quiz } from "./Quiz";
import { Finish } from "./Finish";
function Mainloop(){
const {mode} = useQuiz();
return (
<div className="App">
{mode === 'menu' && <Menu /> }
{mode === 'quiz' && <Quiz /> }
{mode === 'finish' && <Finish />}
</div>
)
}
export {Mainloop};
Ok, useQuiz, wybieramy mode, wyświetlamy (short circuit evaluation) i jedziemy dalej. Domyślnie pokazuje się menu:
import { useRef, useEffect} from 'react';
import { useQuiz, useQuizDispatch } from './QuizContext';
function Menu(){
const {timedQuestions, questionTime, name }= useQuiz();
const dispatch = useQuizDispatch();
const btnRef = useRef(null);
useEffect(() => {
if(name === ""){
btnRef.current.setAttribute("disabled", "");
} else {
btnRef.current.removeAttribute("disabled");
}
}, [name]);
function onQuestionTimeChange(e){
dispatch({type: 'questionTimeChange', payload: e.target.value});
}
return (
<>
<p>Your name: {name}</p>
<input type="checkbox" id="timedQ" checked={timedQuestions} onChange={() => dispatch({type: "toggleTimedQuestions"})} />
<label htmlFor="timedQ">Timed Questions: {timedQuestions ? 'YES' : 'NO'}</label>
<p></p>
<input
disabled={timedQuestions ? false: true}
value={questionTime}
onChange={onQuestionTimeChange}
type="range"
id="questionTime"
min="5" max="15" /> {questionTime}s
<p></p>
<input type="text" onChange={(e) => dispatch({type: "changeName", payload:e.target.value})} value={name}/>
<button ref={btnRef} onClick={() => dispatch({type: "changeMode", payload:"quiz"})}>Start Quiz</button>
</>
)
};
export {Menu};
Jaki bajzel w tym menu, będziemy musieli to posprzątać, wyrzucić do pomniejszych komponentów. Na razie jednak powinniśmy ogarniać co się tu dzieje. I tak jest mniej propsów drillowanych a więcej używania kontekstu i dispatchera, jest czytelniej.
No lepszej zachęty do używania kontekstu i reducerów nie dostaniemy niż porównanie jak rzeczy wyglądają bez tego. To wygląda dużo lepiej, tylko po prostu za bardzo nam się komponent rozrósł.
Ok, to teraz następny przystanek, czyli komponent Quiz:
import QUESTIONS from './questions.js';
import { useState} from 'react';
import { Question } from './Question.js';
import { useQuiz, useQuizDispatch } from './QuizContext.js';
function SimpleSummary({idx, len}){
return (
<>
<p>Question {idx + 1} / {len}</p>
</>
)
}
function Quiz(){
const [userAnswers, setUserAnswers] = useState([]);
const [timerKey, setTimerKey] = useState(1);
const {score, name} = useQuiz();
const dispatch = useQuizDispatch();
const activeQuestionIndex = userAnswers.length;
const quizIsComplete = activeQuestionIndex === QUESTIONS.length;
function handleClick(selectedIdx){
console.log(`Selected answer ${selectedIdx}`)
if(selectedIdx === QUESTIONS[activeQuestionIndex].correctIndex){
dispatch({type: 'updateScore', payload: 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={() => dispatch({type: "changeMode", payload:"finish"})}>Finish</button>}
</>
)
};
export {Quiz};
To jest ostatnie miejsce, gdzie mamy stan. Zachowałem to, bo robienie update stanu i tablicy jako elementu tego stanu może być odrobinę podchwytliwe i nie chciałem tak wrzucać tego wszystkiego na raz.
Docelowo wywalimy i tamten useState, ale na razie go zatrzymamy. I tak jest dużo lepiej. Ok, to teraz Question:
import QUESTIONS from './questions.js';
import { Answers } from './Answers.js';
import Timer from './Timer.js';
import { useQuiz } from './QuizContext';
function Question({index, handleAnswer, timerKey}){
const {timedQuestions, questionTime} = useQuiz();
let timer = questionTime * 1000;
return (
<>
{timedQuestions && <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};
Tutaj używamy stanu z kontekstu już. Ogarnijmy i idziemy do answers:
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};
Nic się nie zmieniło od poprzedniej lekcji, ale już widać, że przydałby się mechanizm do mieszania odpowiedzi, tylko wtedy cała nasza konstrukcja sprawdzania, która odpowiedź jest dobra jest do poprawy.
Ok, to teraz zobaczmy finish:
import { useQuiz, useQuizDispatch } from "./QuizContext";
function Finish(){
const {name, score} = useQuiz();
const dispatch = useQuizDispatch();
function handleRestart(){
dispatch({type: "restart"});
}
return (
<>
<p>Quiz finished!</p>
<p>Your name: {name}</p>
<p>Your score: {score}</p>
<button onClick={handleRestart}>Restart Quiz</button>
</>
)
};
export {Finish};
Można zrobić dwa dispatche, jeden na restart score, drugi na przeskok do menu, ale to bez sensu, więc w reducerze zrobiliśmy sobie akcję restart, która zachowuje poprzedni stan, ale zmienia mode na menu oraz score ustawia na 0.
Ok, projekt nabiera kształtu. Btw, handleRestart teraz jest bez sensu, wrzućmy ten dispatch w onclicka i będziemy kontynuować w następnej lekcji!