【问题标题】:React: why useState magically React.createElement when used with Component?React:为什么 useState 在与 Component 一起使用时神奇地 React.createElement?
【发布时间】:2021-09-10 15:11:14
【问题描述】:

当使用带有FunctionComponentuseState 钩子(即JSX.Element)时,React 会神奇地在组件上调用React.createElement()

虽然不是大问题,但也是意料之中的。

我想有一个样本会更清楚:

function Test() {
  return <div>Hello</div>;
}

function App() {
  const [component, setComponent] = useState();

  return (
    <div className="App">
      <h1>useState with a component</h1>
      <button onClick={() => setComponent(Test)}>Set the state</button>

      <div>
        {component}
      </div>
      <div>
        {Test}
      </div>
    </div>
  );
}

单击按钮时,您会期望 2 个 div 元素呈现相同的内容:它们都应该包含 Test 组件。但实际上并非如此:component 包含 React.createElement(Test)

这里是说明这一点的代码框:https://codesandbox.io/s/react-usestate-with-a-component-oho5b?file=/src/index.js

我看不到任何可以详细说明此行为的内容。

我的问题是:

  • 这是一个错误还是一个功能?
  • 如果这是一个特性,我们能否在未来版本的 React 中依赖它?

【问题讨论】:

  • 问题是......Test 函数在未被调用时如何返回? {Test} 不应该是 {Test()} 吗?
  • 我没有看到任何“问题”该组件是什么。
  • 我也没有发现任何问题。 codesandbox.io/s/… 你称它为 Test() 或 有效。而且你的状态也在起作用。
  • 您正试图在 DOM 中渲染 component,这需要对 createElement 做出反应,以便确定 Test 渲染到什么。你还期望&lt;div&gt;{component}&lt;/div&gt; 做什么?
  • @ElanHamburger 需要明确的是:React 不是自动调用createElement。它实际上根本没有调用它。这是useState 的更新程序工作方式的副作用。

标签: reactjs react-hooks use-state


【解决方案1】:

感谢 cmets,我可以理解发生了什么:这个问题与在 useState 中呈现的组件并没有真正的关系,但这个问题与 useState 与函数一起使用更相关。当useState 与函数一起使用时,将调用传递给setState 的函数。

这在这个问题中有详细说明:is it possible to React.useState(() => {}) in React?

【讨论】:

  • 我不确定 cmets 是否会引导人们朝着正确的方向前进,但您是正确的。本质上,使用组件引用调用状态设置器被视为setState(Test()),因为useState 接受要调用的函数参数。 重要:这不会将Test 视为具有自己生命周期的组件,而是将其视为返回JSX 的普通函数。在状态中存储组件是一种反模式,如果您的 Test 函数使用钩子,您很可能会出错。
【解决方案2】:

我提供这个答案是为了对这里观察到的情况添加额外的解释,并确保清楚为什么会发生这种情况。

第一个误解是,由于&lt;div&gt;{component}&lt;/div&gt; 按预期呈现,它正在转换为React.createElement 调用。事实并非如此。

实际发生了什么

事实上,component 已经持有从Test 返回的 JSX,并且不需要作为函数或组件调用即可渲染。

请参阅下面 sn-p 中的控制台日志以获取差异的运行证明。

const {useState, useEffect} = React;

function Test() {
  return <div>Hello</div>;
}

function App() {
  const [component, setComponent] = useState();

  // Only after button has been pressed
  if (component) {
    console.log('Notice: Test has already been evaluated:', component);
    console.log('Notice: Test has not been evalutated:', Test);
  }
  
  return (
    <div className="App">
      <h1>useState with a component</h1>
      <button onClick={() => setComponent(Test)}>Set the state</button>
      <div>
        {component}
      </div>
      <div>
        {Test}
      </div>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

为了进一步证明component 没有调用React.createElement,下面的sn-p 显示了如果你尝试在Test 中使用钩子会发生什么。它坏了,因为component 实际上不是一个 React 组件,而只是一个在这种情况下碰巧返回 JSX 的普通函数。

const {useState, useEffect} = React;

function Test() {
  useEffect(() => {
    console.log('mount');
  }, []);
  return <div>Hello</div>;
}

function App() {
  const [component, setComponent] = useState();
  
  return (
    <div className="App">
      <h1>useState with a component</h1>
      <button onClick={() => setComponent(Test)}>Set the state</button>
      <div>
        {component}
      </div>
      <div>
        {Test}
      </div>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

它出现“Minified React 错误 #310”的错误,在查找时与错误地使用钩子有关。

为什么component 已经被评估了?

理解这种行为的关键是useStatefunctional updates

要在进行更新时访问之前的状态,您可以向状态更新程序传递一个函数 - setState((prev) =&gt; new)Test 是一个函数,所以当将它传递给 setState 时,React 只会看到你已经传递了一个函数并调用它以确定新的状态 - setState(Test) -> setState(Test())

因此,component 不像看起来那样持有组件引用,它实际上持有调用 Test()返回 值。

结论

  • 在状态中存储组件实例(甚至引用)是一种反模式。希望现在你能明白为什么了。相反,更好的做法是将标识符存储在状态中,并根据这些标识符有条件地渲染组件。

  • React.createElement 只在两种情况下被调用:你使用 JSX 语法,它被转译为creatElement 函数调用;或者你自己明确地调用它。

【讨论】:

  • 感谢您提供如此详细的解释!关于反模式,我也大体上同意。虽然我的用例有点具体:我需要正确处理代码拆分和 SSR...
【解决方案3】:

函数组件是一个类型,好吧,它是一个函数,但是你可以把它当作一个类型。所以

function App() {
  const [Comp, setComponent] = useState();
  const onClick = () => { setComponent(Test) }

  return <div><Comp /></div>
}

当然,如果你想要元素,你可以这样做

function App() {
  const [comp, setComponent] = useState();
  const onClick = () => { setComponent(<Test />) }

  return <div>{comp}</div>
}

无论哪种方式都可以。

注意:这有点令人困惑。函数组件确实会返回所有元素,在内部它会在返回它们之前调用createElement。但函数组件本身并不是 DOM 元素。在内部,它将作为一种特殊标识“纤维”附加,以帮助引擎决定如何渲染。您可以将函数组件称为函数,因为这就是引擎将其用作光纤的type 的方式。

【讨论】:

    猜你喜欢
    • 2021-10-04
    • 2022-07-06
    • 2021-06-07
    • 2022-01-06
    • 1970-01-01
    • 2022-11-23
    • 1970-01-01
    • 2020-05-03
    • 2020-09-24
    相关资源
    最近更新 更多