Projekt roboczy, będzie quiz w całości to będzie tutorial w całości, na razie kodujemy wspólnie ten quiz. Timer już był pokazany, to teraz pokazujemy, co się zmieniło.
Ok, zanim zrobimy refactoring to App przyjmuje, czy pokazywać dobre odpowiedzi czy nie:
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App2 usingTimer={false} />
</React.StrictMode>
);
Ok importy:
import LIST2 from './utils/list2';
import './App.css';
import { SelectableList3 } from './components/SelectableList3';
import { useState, useEffect } from 'react';
import { Question } from './components/Question';
import { Score } from './components/Score';
import { SimpleSummary } from './components/SimpleSummary';
import Timer from './components/Timer';
Question:
import LIST2 from "../utils/list2";
function Question({idx}){
return (
<h2>{LIST2[idx].question}</h2>
)
}
export {Question};
Score:
function Score({score}){
return (
<p>Score: {score}</p>
)
};
export {Score};
Summary:
import LIST2 from "../utils/list2"
function SimpleSummary({idx}){
return (
<>
<p>Question {idx + 1} / {LIST2.length}</p>
</>
)
};
export {SimpleSummary};
Timer już omówiony, to teraz App. Nad app mamy funkcję pomocniczą:
function prepareAnswer(idx, answer){
let questionNumber = idx;
let chosenAnswer = answer;
let isCorrect = LIST2[idx].correct === answer;
return {questionNumber, chosenAnswer, isCorrect};
}
Ona formatuje odpowiedzi. Teraz stan App:
function App2({usingTimer}) {
const [index, setIndex] = useState(0);
const [score, setScore] = useState(0);
const [answers, setAnswers] = useState([]);
const [timerBlocked, setTimerBlocked] = useState(false);
const maxIndex = LIST2.length;
Indeks, score, odpowiedzi, czy timer ma być zblokowany. Mamy też maxIndex.
Ok, setNextQuestion, już powoli zaczyna nabierać kształtu:
function setNextQuestion(ans){
let answered = prepareAnswer(index, ans);
setAnswers(prev => [...prev, answered]);
if(answered.isCorrect === true){
setScore((s) => s + 1);
}
if(index < maxIndex)
setIndex((i) => i + 1);
setTimerBlocked(false);
}
SkipQuestion raczej proste:
function skipQuestion(){
setAnswers(prev => [...prev, null]);
if(index < maxIndex)
setIndex((i) => i + 1);
}
Debugujemy odpowedzi useEffectem:
useEffect(() => {
console.log(answers);
}, [answers]);
console.log(index, maxIndex);
let shouldRenderQuestion = true;
if(index === maxIndex){
shouldRenderQuestion = false;
}
Od razu wrzuciłem takie coś, żebym mógł sprawdzać, czy mam wyświetlić pytanie. Po refaktorze to będą osobne komponenty, na razie trzeba było zrobić w ogóle funkcjonujący jako tako prototyp.
To teraz JSX:
return (
<div className="App">
{shouldRenderQuestion && usingTimer ?
<Timer
key={index+1}
timeout={5000}
onTimeout={timerBlocked ? null : skipQuestion}
blocked={timerBlocked}
/> : null }
<Score score={score} />
{shouldRenderQuestion ? <Question idx={index} /> : null}
{shouldRenderQuestion ? <SelectableList3
idx={index}
shuffle={true}
key={index}
setTimerBlocked={setTimerBlocked}
setNextQuestion={setNextQuestion }
skipQuestion={skipQuestion}
showCorrect={true}/> : "Quiz Finished"}
{shouldRenderQuestion ? <SimpleSummary idx={index} /> : null}
</div>
);
}
export default App2;
Jak już rozkminimy co jest co i dlaczego to pokazuję selectablelist:
import { useRef, useState, useEffect } from "react";
import LIST2 from "../utils/list2";
function SelectableList3({idx, shuffle, setNextQuestion, skipQuestion, setTimerBlocked, showCorrect}){
const answers = useRef(null);
const correct = useRef(null);
const chosenAnswerRef = useRef(null);
const [mode, setMode] = useState(null);
if(correct.current === null){
correct.current = LIST2[idx].correct;
}
if(answers.current === null){
const rawAnswers = LIST2[idx].answers;
const readyAnswers = [...Object.entries(rawAnswers)];
if(shuffle){
readyAnswers.sort(() => Math.random() - 0.5);
}
answers.current = [...readyAnswers];
}
Chyba proste. SelectableList ma unmount po każdym (przez key na index) i to wiele ułatwia, bo te refy będą po każdym pytaniu nullem. Tu sobie przypisujemy odpowiedź prawidłową oraz odpowiedzi sobie parsujemy, mieszamy i zwracamy, już gotowe do używania.
ChosenAnswerRef ustawiamy na onClick zaś mode to mode, w effect zobaczymy. Warto też zwrócić uwagę, że bierzemy skipQuestion, setNextQuestion, oraz setTimerBlocked jako funkcje w propsach.
Mamy też showCorrect jako props, on pochodzi z App. Bez showCorrect nie ma pokazywania czy odpowiedź jest ok czy nie.
Dobra, to teraz zobaczmy effect:
useEffect(() => {
let timer;
switch(mode){
case null:
break;
case 'chosen':
chosenAnswerRef.current.className = 'selectedAnswer';
if(showCorrect === false){
setTimerBlocked(true);
timer = setTimeout(() => {
setNextQuestion(chosenAnswerRef.current.dataset.idx);
},700)
break;
}
setMode('waiting');
setTimerBlocked(true);
break;
case 'waiting':
timer = setTimeout(() => {
let cls = chosenAnswerRef.current.dataset.idx === correct.current ? 'correctAnswer' : 'wrongAnswer';
chosenAnswerRef.current.className = cls;
setMode('feedbackSent')
},2000);
break;
case 'feedbackSent':
timer = setTimeout(() => {
setNextQuestion(chosenAnswerRef.current.dataset.idx);
},1000);
break;
default:
break;
}
return () => {
clearTimeout(timer);
};
}, [mode, setNextQuestion, setTimerBlocked, showCorrect]);
Mega się namęczyłem, ale jestem zadowolony. Oczywiście nie zrozumiemy tego, jeżeli nie zobaczymy co tę maszynerię wprawia w ruch:
function handleClick(e){
if(chosenAnswerRef.current === null){
chosenAnswerRef.current = e.target;
setMode('chosen');
}
}
No i teraz JSX, czyli najmniejsza trudność jak się okazuje:
return (
<>
<ul>
{answers.current.map((item) => {
const [key,value] = item;
return (
<li
key={key}
data-idx={key}
onClick={(e) => handleClick(e)}
>{value}</li>)
})}
</ul>
<button
disabled={mode !== null}
onClick={() => skipQuestion()}
>Skip</button>
</>
);
}
export {SelectableList3};
Co jest piękne:
- Możemy przestawiać sobie czy ma pokazywać prawidłowe odpowiedzi czy nie
- Możemy przestawiać, czy quiz ma być z timerem, czy nie
- Możemy przestawiać, czy ma być shuffle, czy nie
- Niezależnie od kombinacji naszych ustawień, wszystko działa jak należy, bez zarzutu
Co jest słabe:
- Mało modularne, trzeba to będzie rozbić na komponenty, takie Quiz, Question, Answers, Timer i tak dalej
- Nie mamy formularza i kontekstu jeszcze
Dodam, że poprzednio robiliśmy wszystko w kontekście i to w React Profiler pokazało, że co każdą tycią zmianę wszystko objęte kontekstem się re-renderowało (czyli cała apka).
Teraz bawimy się w useStejty i drillowanie oraz wynoszenie, to żadna hańba. Przyznam, że nie do końca to ogarniałem, ale generalnie context i inne hooki są po inne rzeczy. Nie ma tak, że context i reducer są lepsze od useState i drillowania.
Niedługo będzie cała apka, tworzymy razem albo czekamy na tutorial już bardziej poukładany.