The QuizBuzz
app must remember a user's responses to properly set the inputs when the user returns to an earlier question. Since a single Question
component serves all the questions, local state is not appropriate. This sounds like a job for a global store managed by Redux.
You set Redux up much like you did with QuoteBase and the Unforget client. You first define a couple of actions for starting a new quiz and setting the user's response to a question in actions.js
:
export const Action = Object.freeze({
SetResponse: 'SetResponse',
StartQuiz: 'StartQuiz',
});
export function setResponse(slug, response) {
return {
type: Action.SetResponse,
payload: {slug, response},
};
}
export function startQuiz(slug) {
return {
type: Action.StartQuiz,
payload: slug,
};
}
Then you define the store and a reducer that responds to these actions in store.js
:
import {createStore} from 'redux';
import {Action} from './actions';
function reducer(state, action) {
switch (action.type) {
case Action.SetResponse:
return {
...state,
responses: {
...state.responses,
[action.payload.slug]: action.payload.response,
},
};
case Action.StartQuiz:
return {
...state,
responses: {},
quizSlug: action.payload,
};
default:
return state;
}
}
const initialState = {
quizSlug: null,
responses: {},
};
export const store = createStore(reducer, initialState);
The state is just two entities: the slug of the quiz currently being taken and an object that holds onto the user's responses to the current quiz. The responses object is keyed on the question slugs. When a new quiz is started, the responses object is emptied.
In index.js
, you inject a Provider
into the component hierarchy:
// ...
import {Provider} from 'react-redux';
import {store} from './store';
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
When the user clicks the start button in the Quiz
component, you dispatch a startQuiz
action to reset the responses:
// ...
import {useDispatch} from 'react-redux';
import {startQuiz} from './actions';
export function Quiz(props) {
// ...
const dispatch = useDispatch();
return (
<div className="quiz-page">
{/* ... */}
<button
onClick={() => {
dispatch(startQuiz(quiz.slug));
navigate(`/quiz/${quiz.slug}/question/${quiz.questions[0].slug}`);
}}
>Start</button>
</div>
);
}
The Question
component requires a bit more work because it must both dispatch actions and consume the state. You grab a reference to the dispatch
function and select out the responses object:
// ...
import {useDispatch, useSelector} from 'react-redux';
import {setResponse} from './actions';
export function Question(props) {
// ...
const dispatch = useDispatch();
const responses = useSelector(state => state.responses);
// ...
}
When you present the response inputs, you populate their values using any previously recorded response. For the false radio button, you have to be careful with how you set its checked
prop. If you express the prop as checked={!responses[question.slug]}
, then the button will be checked even if the user hasn't responded. That's because the lookup will yield undefined
, and !undefined
in JavaScript is true. So, you check for an explicit true or false value using ===
. If no answer has yet been given for a fill-in-the-blank question, you default to an empty string. For both kinds of inputs, you add an onChange
event listener that dispatches a setResponse
action:
export function Question(props) {
// ...
let inputs;
if (question.type === 'true-false') {
inputs =
<div className="response">
<label>
<input
type="radio"
name="group"
checked={responses[question.slug] === true}
onChange={() => dispatch(setResponse(question.slug, true))}
/>
True
</label>
<label>
<input
type="radio"
name="group"
checked={responses[question.slug] === false}
onChange={() => dispatch(setResponse(question.slug, false))}
/>
False
</label>
</div>;
} else if (question.type === 'blank') {
inputs =
<div className="response">
<input
type="text"
value={responses[question.slug] || ''}
onChange={event => {
dispatch(setResponse(question.slug, event.target.value));
}}
/>
</div>;
}
// ...
}
Try working your way through the questions of a quiz now. Do the response inputs clear when you visit a new question? Are they restored to the user's previous answer when you navigate back?