防止功能道具更新时重新渲染

Prevent rerender on function prop update

我有一个包含多层子组件的表单。表单的状态保持在最高级别,我将函数作为 props 向下传递以更新顶层。唯一的问题是当表单变得非常大(您可以动态添加问题)时,每个组件都会在其中一个组件更新时重新加载。这是我的代码的简化版本(或 codesandbox:https://codesandbox.io/s/636xwz3rr):

const App = () => {
  return <Form />;
}

const initialForm = {
  id: 1,
  sections: [
    {
      ordinal: 1,
      name: "Section Number One",
      questions: [
        { ordinal: 1, text: "Who?", response: "" },
        { ordinal: 2, text: "What?", response: "" },
        { ordinal: 3, text: "Where?", response: "" }
      ]
    },
    {
      ordinal: 2,
      name: "Numero dos",
      questions: [
        { ordinal: 1, text: "Who?", response: "" },
        { ordinal: 2, text: "What?", response: "" },
        { ordinal: 3, text: "Where?", response: "" }
      ]
    }
  ]
};

const Form = () => {
  const [form, setForm] = useState(initialForm);

  const updateSection = (idx, value) => {
    const { sections } = form;
    sections[idx] = value;
    setForm({ ...form, sections });
  };

  return (
    <>
      {form.sections.map((section, idx) => (
        <Section
          key={section.ordinal}
          section={section}
          updateSection={value => updateSection(idx, value)}
        />
      ))}
    </>
  );
};

const Section = props => {
  const { section, updateSection } = props;

  const updateQuestion = (idx, value) => {
    const { questions } = section;
    questions[idx] = value;
    updateSection({ ...section, questions });
  };

  console.log(`Rendered section "${section.name}"`);

  return (
    <>
      <div style={{ fontSize: 18, fontWeight: "bold", margin: "24px 0" }}>
        Section name:
        <input
          type="text"
          value={section.name}
          onChange={e => updateSection({ ...section, name: e.target.value })}
        />
      </div>
      <div style={{ marginLeft: 36 }}>
        {section.questions.map((question, idx) => (
          <Question
            key={question.ordinal}
            question={question}
            updateQuestion={v => updateQuestion(idx, v)}
          />
        ))}
      </div>
    </>
  );
};

const Question = props => {
  const { question, updateQuestion } = props;

  console.log(`Rendered question #${question.ordinal}`);

  return (
    <>
      <div>{question.text}</div>
      <input
        type="text"
        value={question.response}
        onChange={e =>
          updateQuestion({ ...question, response: e.target.value })
        }
      />
    </>
  );
};

我试过使用 useMemo 和 useCallback,但我不知道如何让它工作。问题是向下传递函数以更新其父项。如果不在每次表单更新时更新它,我不知道该怎么做。

我无法在任何地方在线找到解决方案。也许我正在寻找错误的东西。感谢您提供的任何帮助!

解决方案

使用 Andrii-Golubenko's answer and this article React Optimizations with React.memo, useCallback, and useReducer 我想出了这个解决方案: https://codesandbox.io/s/myrjqrjm18

请注意控制台日志如何仅显示已更改组件的重新呈现。

我建议使用生命周期方法来防止重新渲染,在你可以使用的反应挂钩示例中,useEffect。此外,将您的状态集中在上下文中并使用 useContext 挂钩可能也会有所帮助。

  1. 如果 props 未更改,则对功能组件使用 React 功能 React.memo ,类似于 class 组件的 PureComponent。
  2. 当你像那样传递回调时:
<Section
    ...
    updateSection={value => updateSection(idx, value)}
/>

你的组件 Section 每次父组件重新渲染时都会重新渲染,即使其他道具没有改变并且你使用 React.memo 也是如此。因为每次父组件渲染时你的回调都会 re-create 。您应该将回调包装在 useCallback 挂钩中。

  1. 如果您需要存储像 initialForm 这样的复杂对象,使用 useState 不是一个好的决定。最好使用 useReducer;

在这里您可以看到可行的解决方案:https://codesandbox.io/s/o10p05m2vz

用一个复杂的表单解决这个问题,我实施的技巧是在组件的输入元素上使用 onBlur={updateFormState} 来触发通过作为 prop 传递的函数将表单数据从组件提升到父表单到组件。

为了更新组件的输入元素,我使用 onChange={handleInput} 使用组件内的状态,然后在输入时将组件状态传递给提升函数(或整个组件,如果有多个组件中的输入字段)失去焦点。

这有点老套,而且由于某种原因可能是错误的,但它在我的机器上运行良好。 ;-)