简体   繁体   中英

React hook useCallback to avoid multiple renders

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>
  );
}

编辑 smart-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>
  );
}

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM