So here's my issue, im currently learning about the useReducer function and trying to utilize it to create a react based quiz, selection screen. I wanted to update the state (so it could be uplifted to the main App component for use elsewhere)
The problem im running into, is that I cannot use the reducer from outside the component (it won't let me set the state) and inside the component it works and uplifts the state but i get this error:
Cannot update a component (
App) while rendering a different component (
GetQuestionDATA)
This is my file for GetQuestionData:
import { useReducer } from "react";
import React from "react";
import Select from "react-select";
export function GetQuestionDATA({ setAPIParams }) {
// USE REDUCER to get our various Data from state shown on screen.
// set initial state
const initialState = {
amount: "10",
category: "",
difficulty: "",
type: "",
};
// set the Amount, with reducer
const [state, dispatch] = useReducer(reducer, initialState);
function reducer(state, action) {
// we don't tend to use ifs or terniary in reducers instead we look at using the switch keyword, which takes our action type and creates a case for each type.
switch (action.type) {
case "setAmount":
return { ...state, amount: action.payload };
case "setCategory":
return { ...state, category: action.payload };
case "setDifficulty":
return { ...state, difficulty: action.payload };
case "setType":
return { ...state, type: action.payload };
case "start":
return setAPIParams(state);
default:
throw new Error("Unknown action");
}
}
const defineAmount = function (e) {
dispatch({ type: "setAmount", payload: Number(e.target.value) });
};
const defineCategory = function (e) {
dispatch({ type: "setCategory", payload: e.value });
};
const defineDifficulty = function (e) {
dispatch({ type: "setDifficulty", payload: e.value });
};
const defineType = function (e) {
dispatch({ type: "setType", payload: e.value });
};
const start = function () {
dispatch({ type: "start" });
};
// set state based on choices (dispatch + action)
// Buncle the state into an object (reducer)
//----- jsx options -----//
// category
const category_options = [
{ value: "9", label: "General Knowledge" },
{ value: "10", label: "Entertainment: Books" },
{ value: "11", label: "Entertainment: Film" },
{ value: "12", label: "Entertainment: Music" },
{ value: "13", label: "Entertainment: Musical & Theaters" },
{ value: "14", label: "Entertainment: Television" },
{ value: "15", label: "Entertainment: Video Games" },
{ value: "16", label: "Entertainment: Board Games" },
{ value: "17", label: "Science & Nature" },
{ value: "18", label: "Science: Computers" },
{ value: "19", label: "Science: Mathmatics" },
{ value: "20", label: "Mythology" },
{ value: "21", label: "Sports" },
{ value: "22", label: "Geography" },
{ value: "23", label: "History" },
{ value: "24", label: "Politics" },
{ value: "25", label: "Art" },
{ value: "26", label: "Celebrities" },
{ value: "27", label: "Animals" },
];
// difficulty
const difficulty_options = [
{ value: "easy", label: "Easy" },
{ value: "medium", label: "Medium" },
{ value: "hard", label: "Hard" },
];
// type
const type_options = [
{ value: "multiple", label: "Multiple Choice" },
{ value: "boolean", label: "True / False" },
];
// encoding (hidden)
return (
<div className="quiz-selection">
<p>Number Of Questions</p>
<div className="quiz-buttons">
<span>
<button value="10" onClick={defineAmount}>
10
</button>
</span>
<span>
<button value="20" onClick={defineAmount}>
20
</button>
</span>
<span>
<button value="30" onClick={defineAmount}>
30
</button>
</span>
<span>
<button value="40" onClick={defineAmount}>
40
</button>
</span>
<span>
<button value="50" onClick={defineAmount}>
50
</button>
</span>
</div>
<div className="select">
<p className="cat-header">Category</p>
<Select onChange={defineCategory} options={category_options} />
<p className="cat-header">Difficulty</p>
<Select onChange={defineDifficulty} options={difficulty_options} />
<p className="cat-header">Type</p>
<Select onChange={defineType} options={type_options} />
</div>
<div>
<button className="quiz-buttons" onClick={start}>
Start
</button>
</div>
</div>
);
}
For additional context I know I probably could fix this with context and redux, but im not that far along yet so would rather try and fix the current problem with what I know.
I tried to utilize useEffects, however I cannot wrap a reducer in a callback according to the console.
I also tried moving the reducer in and out of the component, however this means that i either get an error (the state still uplifts) or the setState cannot be used and I can't uplift said state
Edit: Fixed the problem, I created a separate state inside the component, and then told the reducer to update that instead of trying to uplift it. I then wrapped the setter for the main state in a useEffect.
A couple of things:
setAPIParams(state)
this will just cause confusion. In fact, a key tenet of reducers is that they never use or change any external variables. They are only concerned with their own state. A better approach for lifting the state would be to call the useReducer
hook in a parent component.So, here's the steps you could take to fix this:
case "start": return setAPIParams(state);
from your reducer. You could store the "start" or "stop" state in a separate variable (using useState
). It's not really related to the rest of the state that the reducer manages, so you can handle it seperately.useReducer
into the parent component as well as all the actions (defineAmount
, defineCategory
etc...). These should be inside the component.I made an example solution in a codesandbox. I mostly followed the steps I listed above, but instead of moving the reducer logic to the parent component, I put it in a custom hook. This just simplifies the components. Also, I put comments everywhere explaining how everything works!