避免的常见模式:破坏 React

我绝不是一个专家级的 React 工程师,但我有几年的经验。React 是一个用于构建用户界面的强大库,但在某些地方也很脆弱。我遇到过的一个常见 bug 是由于直接操作 DOM 结合 React而引起的。这是一种反模式,因为在适当的情况下,它可能会破坏整个 React 应用程序,并且很难进行调试。

在我们深入解释问题及其解决方法之前,这里有一个最小示例可以重现此 bug:

const destroyElement = () =>
  document.getElementById('app').removeChild(document.getElementById('my-div'));

const App = () => {
  const [elementShown, updateElement] = React.useState(true);

  return (
    <div id='app'>
      <button onClick={() => destroyElement()}>
        通过 querySelector 删除元素
      </button>
      <button onClick={() => updateElement(!elementShown)}>
        更新元素和状态
      </button>
    { elementShown ? <div id="my-div">我是元素</div> : null }
    </div>
  );
};

ReactDOM.createRoot(document.getElementById('root')).render(
  <App />
);

这是一个相当简单的 React 应用程序,有一个容器、两个按钮和一个状态变量。问题在于,如果你点击调用 destroyElement() 的按钮,然后再点击另一个按钮,应用程序将崩溃。你可能会问,为什么会这样?问题可能并不明显,但如果你查看浏览器控制台,你会注意到以下异常信息:

Uncaught DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.

这可能仍然有些晦涩,所以让我解释一下发生了什么。React 使用自己的 DOM 表示形式,称为虚拟 DOM,以确定要渲染的内容。通常,虚拟 DOM 会与当前的 DOM 结构匹配,React 会处理属性和状态的变化。然后,它会更新虚拟 DOM,并批量发送必要的更改到真实的 DOM。

然而,在这种情况下,React的虚拟DOM和真实DOM是不同的,因为destroyElement()移除了#my-div元素。因此,当React尝试使用虚拟DOM的更改更新真实DOM时,由于元素不存在,无法删除该元素。这导致上述异常被抛出,你的应用程序出现错误。

你可以将destroyElement()重构为App组件的一部分,并与其状态进行交互,以修复此示例中的问题。无论问题或解决方案有多简单,它都展示了在某些情况下React是多么脆弱。在一个大型代码库中,每天有许多开发人员在不同的领域贡献代码,这种情况只会更加复杂。在这样的环境中,这样的问题很容易引入,追踪它们可能会非常棘手。这就是为什么我建议在与React结合使用时要非常小心直接操作DOM。