React hook useCallback 避免多次渲染

React hook useCallback to avoid multiple renders

我正在尝试构建一个简单的 Material UI Stepper 以允许用户点击 NextBack,以及这一步,但它会触发 reducer 两次。

我读过 somewhere 解决这个问题的方法是 useCallbackuseMemo 钩子,它可以避免一个函数多次实例化,只有在它发生变化时才返回函数或结果。

我的问题是,这个例子仍然很清楚,但我不确定如何将其应用到我的代码中。我正要使用简单的状态管理,效果很好。但是我想学这个...

这是我的 App 函数:

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

我已经尝试过此代码,但仍然无法正常工作,因为它在调用 dispatchActiveStep() 时仍会重新呈现:

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

下面是使用 useReducer 的解决方案:CodeSandbox

我不确定你的组件被重新渲染两次的确切原因,但 useStateuseReducer 的混合以及减速器中的副作用对我来说似乎是错误的,所以我决定只使用 useReducer 重写它。可能是因为 dispatchActiveStepsetCompleted 而渲染了两次,因为它们都触发了重新渲染。

编辑:实际上重新渲染的原因是您在组件内部定义了一个减速器,并且每次都会重新创建它: