Search code examples
javascriptreactjsobjectreact-hookssetstate

Is it possible to update state within a useReducer


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.


Solution

  • A couple of things:

    • Your reducer should be defined outside the component.
    • You shouldn't store the state from the reducer in another state variable like you are doing with 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:

    • Remove 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.
    • Move the reducer into the parent component's file, but put it at the base level, not inside the component.
    • Move useReducer into the parent component as well as all the actions (defineAmount, defineCategory etc...). These should be inside the component.
    • Once you define the actions in the parent component, you can pass them down to the GetQuestionDATA component as props.

    Here's my solution

    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!