Persisting Responses

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?