1

I am trying to build a simple Material UI Stepper to allow a user clicks on Next and Back, and also in the step, but it triggers the reducer twice.

I have read somewhere that the solution to this is useCallback or useMemo hook which avoids a function from instantiation more than once returnig the function or result only if it changes.

My problem is that still the clear example I am not sure how to apply this to my code. I was about to use simple state management which works great. But I would like to learn this...

This is my App function:

function App() {
  const [completed, setCompleted] = React.useState({});
  const [activeStep, dispatchActiveStep] = React.useReducer((step, action) => {
    let completedSteps = completed;
    let active = step;
    switch (action.type) {
      case "next":
        if (step < steps.length) {
          completedSteps[activeStep] = true;
          active = step + 1;
        }
        break;
      case "previous":
        if (step > 0) {
          delete completed[activeStep];
          active = step - 1;
        }
        break;
      case "set":
        if (!(action.step in Object.keys(completed))) {
          console.error("step not completed");
          return step;
        }
        if (action.step === 0) {
          completedSteps = {};
          active = 0;
        } else if (action.step === steps.length - 1) {
          completedSteps = {};
          for (let i = 0; i <= action.step; i++) {
            completedSteps[i] = true;
          }
          active = action.step;
        }
        break;
      default:
        console.error("action not available");
    }
    console.log("test");
    setCompleted(completedSteps);
    return active;
  }, 0);

  return (
    <Paper>
      <Stepper activeStep={activeStep}>
        {steps.map((step, i) => (
          <Step key={i}>
            <StepButton
              key={i}
              completed={completed[i]}
              onClick={() => dispatchActiveStep({ type: "set", step: i })}
            >
              <Typography>{step.label}</Typography>
            </StepButton>
          </Step>
        ))}
      </Stepper>
      {steps.map((step, i) => {
        if (activeStep === i) {
          return (
            <div key={i} style={styles.content}>
              {step.component}
            </div>
          );
        }
      })}
      <div style={styles.buttons}>
        <Button
          color="primary"
          variant="contained"
          onClick={() => dispatchActiveStep({ type: "previous" })}
          disabled={activeStep === 0}
        >
          Previous
        </Button>
        <Button
          color="secondary"
          variant="contained"
          style={{ marginLeft: "10px" }}
          onClick={() => dispatchActiveStep({ type: "next" })}
          disabled={activeStep === steps.length - 1}
        >
          Next
        </Button>
      </div>
    </Paper>
  );
}

Edit intelligent-wu-geq3d

I have tried this code but still does not work since it still re-renders when the dispatchActiveStep() is called:

function App() {
  const [completed, setCompleted] = React.useState({});
  const [activeStep, setActiveStep] = React.useState(0);

  const handleBack = () => {
    let completedSteps = completed;
    if (activeStep === steps.length - 1) {
      delete completedSteps[activeStep - 1];
    } else {
      delete completedSteps[activeStep];
    }
    setCompleted(completedSteps);
    setActiveStep(activeStep - 1);
  };

  const handleNext = () => {
    let completedSteps = completed;
    completedSteps[activeStep] = true;
    setCompleted(completedSteps);
    setActiveStep(activeStep + 1);
  };

  const handleClick = step => {
    let completedSteps = completed;
    if (!(step in Object.keys(completedSteps))) {
      console.error("step not completed");
      return;
    }
    completedSteps = {};
    for (let i = 0; i < step; i++) {
      completedSteps[i] = true;
    }
    setActiveStep(step);
    setCompleted(completedSteps);
  };

  return (
    <Paper>
      <Stepper activeStep={activeStep}>
        {steps.map((step, i) => (
          <Step key={i}>
            <StepButton
              key={i}
              completed={completed[i]}
              onClick={() => {
                handleClick(i);
              }}
            >
              <Typography>{step.label}</Typography>
            </StepButton>
          </Step>
        ))}
      </Stepper>
      {steps.map((step, i) => {
        if (activeStep === i) {
          return (
            <div key={i} style={styles.content}>
              {step.component}
            </div>
          );
        }
      })}
      <div style={styles.buttons}>
        <Button
          color="primary"
          variant="contained"
          onClick={handleBack}
          disabled={activeStep === 0}
        >
          Previous
        </Button>
        <Button
          color="secondary"
          variant="contained"
          style={{ marginLeft: "10px" }}
          onClick={handleNext}
          disabled={activeStep === steps.length - 1}
        >
          Next
        </Button>
      </div>
    </Paper>
  );
}
Maramal
  • 3,145
  • 9
  • 46
  • 90
  • Why do you need a reducer for this? You can totally do this with second useState and some function handler – mikheevm Sep 29 '19 at 18:20
  • @mikheevm, I used state before but, isn't a good practice using a reducer for something like this? – Maramal Sep 29 '19 at 18:24
  • For two state variables? I wouldn't say so. I'd expect `useReducer` to be used inside a component with some complex logic or a lot of variables. In your case, what you only need is one array of completed steps and an integer of the current step. – mikheevm Sep 29 '19 at 18:28
  • Okay, I will go back to use simple state management. Still I would like to learn how to use `useCallback` and `useReducer` hooks. – Maramal Sep 29 '19 at 18:32

1 Answers1

0

Here is the solution using useReducer: CodeSandbox

I'm not sure about exact reasons why your component was re-rendered twice, but the mix of useState and useReducer and also side effects in the reducer seemed wrong to me, so I decided to rewrite it using useReducer only. Probably it was rendered twice because of the dispatchActiveStep and setCompleted since they both trigger a re-render.

Edit: actually the reason for re-render is that you define a reducer inside the component and it gets recreated every time: useReducer Action dispatched twice

mikheevm
  • 559
  • 5
  • 14
  • okay, so the callback is not needed... I will keep reading about it XD – Maramal Sep 29 '19 at 20:01
  • 1
    Try this: https://kentcdodds.com/blog/usememo-and-usecallback Also I recommend following Kent C. Dodds, he is doing very great job explaining things in React ecosystem – mikheevm Sep 29 '19 at 20:11