在 JSX 中使用 {renderChild()} 和 <Child></Child>有什么区别?

作者站长头像
站长
· 阅读数 8

如题,最近遇到一个bug,问题是出现在react hook 中,为了代码的可读性,我把一个view拆成了多个子 component,其中有一些是 pure component,有一些是含有 usestate 的 stateful component,但是在实践的过程中发现,当父组件使用 setState 更新视图的时候,有些 stateful component 中的 state 会自动还原为初始值,有一些则不会,仔细对比后发现是组件实例化的写法不一样,代码概要如下:

import { useState } from "react";
import CHild from "./Child";
import "./styles.css";

export default function App() {
  const [random, setRandom] = useState(() => Math.random() * 10);

  const RenderPart = function () {
    const [count, setCount] = useState(0);
    return (
      <div>
        {count}
        <button onClick={() => setCount((prev) => prev + 1)}>add one</button>
      </div>
    );
  };

  const renderOtherPart = function () {
    const [count, setCount] = useState(0);
    return (
      <div>
        {count}
        <button onClick={() => setCount((prev) => prev + 1)}>add2 one</button>
      </div>
    );
  };

  return (
    <div className="App">
      <h3>{random}</h3>
      <button
        onClick={() => {
          setRandom(Math.random() * 10);
        }}
      >
        refresh
      </button>

      {/* render component via other render function with Component style would refresh the state of child component*/}
      <RenderPart></RenderPart>

      {/* render component via other render function with function style wouldn't refresh the state neither */}
      {renderOtherPart()}
    </div>
  );
}

使用 Babel 转译后的代码:

  1. 使用 <CHild></CHild> 的方式
"use strict";

function CHild() {
  const [count, setCount] = useState(0);
  return /*#__PURE__*/React.createElement("div", null, count, /*#__PURE__*/React.createElement("button", {
    onClick: () => setCount(prev => prev + 1)
  }, "add one"));
}
function App() {
  const [random, setRandom] = useState(() => Math.random() * 10);
  const RenderPart = function () {
    // return CHild();
    return /*#__PURE__*/React.createElement(CHild, null);
  };
  return /*#__PURE__*/React.createElement("div", {
    className: "App"
  }, /*#__PURE__*/React.createElement("h3", null, random), /*#__PURE__*/React.createElement("button", {
    onClick: () => {
      setRandom(Math.random() * 10);
    }
  }, "refresh"), /*#__PURE__*/React.createElement(RenderPart, null));
}
  1. 使用 renderOtherPart 的方式
"use strict";

function CHild() {
  const [count, setCount] = useState(0);
  return /*#__PURE__*/React.createElement("div", null, count, /*#__PURE__*/React.createElement("button", {
    onClick: () => setCount(prev => prev + 1)
  }, "add one"));
}
function App() {
  const [random, setRandom] = useState(() => Math.random() * 10);
  const renderOtherPart = function () {
    // return CHild();
    return /*#__PURE__*/React.createElement(CHild, null);
  };
  return /*#__PURE__*/React.createElement("div", {
    className: "App"
  }, /*#__PURE__*/React.createElement("h3", null, random), /*#__PURE__*/React.createElement("button", {
    onClick: () => {
      setRandom(Math.random() * 10);
    }
  }, "refresh"), renderOtherPart());
}
在线复现代码地址:在 JSX 中使用 {renderChild()} 和 <Child></Child>有什么区别?

现在问题是不知道为什么以 <Child></Child> 这种方式实例化组件会刷新组件的state,而 {renderChild()} 这种则不会。

--------------- 补充 ------------------

从 babel 转译的结果上来看,区别在于使用 <RenderPart></RenderPart> 的时候会多用一个 React.createElement(RenderPart),在 RenderPart 里面才使用了 React.createElement(Child)。相对于使用 {renderOtherPart()} 的方式,则是只使用了一次 React.createElement(Child),并没有中间的那层 RenderPart。

现在我的猜测是正是因为多出来的这个 RenderPart,因为是在 hook 组件里的,当父组件 setState 的时候,RenderPart 会被重新创建,内存地址改变,在 react 的 diff 的时候判断为删除了旧组件然后又添加了一个新组件,从而触发了更新逻辑。

为了验证我把 RenderPart 从 hook 组件中提取到外部,使它保持不变,或者使用 useCallBack 或者 useMemo 来对 RenderPart 缓存起来,结果验证果然不会重新重置子组件的 state。

回复
1个回答
avatar
test
2024-07-12

就以这段代码为说明

import { useCallback, useState } from "react";
import CHild from "./Child";
import "./styles.css";

export default function App() {
  const [random, setRandom] = useState(() => Math.random() * 10);

  const RenderPart = function () {
    console.log("renderPart RenderPart");
    return <CHild></CHild>;
  };

  const renderPart = RenderPart;
  // console.log("renderPart", renderPart);
  return (
    <div className="App">
      <h3>{random}</h3>
      <button
        onClick={() => {
          setRandom(Math.random() * 10);
        }}
      >
        refresh
      </button>
      <RenderPart></RenderPart>
    </div>
  );
}

为什么每次点击refresh子组件状态都会被重置,他的原理非常简单应为组件的类型变了函数组件的类型就是函数,点击按钮后RenderPart作为一个局部变量会被重新创建他和前一次更新时使用的变量已经不是同一个了,这种变化和由div变为了p是完全等价的所以之前的RenderPart会被unmount,新的RenderPart会被mount,他的状态会回到初始状态,而没有走更新逻辑,所以你会发现只要在RenderPart外面套一层useMemo就能解决问题,具体的代码可以去看源代码(1) 进行RenderPart的diff(2) 发现前后的函数组件不同(3) 发现并没有复用前一轮更新时的fiber,删除老的,mount新的

还有一个大问题你要注意不要在创建并返回jsx的函数直接用hook,如果要用请将该函数用use开头命名比如你的这个代码会导致React缺失对renderOtherPart Hook的调用顺序和数量的检查这回导致React脆弱的Hook系统崩溃,如果哪天你在外面加一个判断条件再一次render中有时调用renderOtherPart有时又不调用这回直接会把你的程序搞挂,所以像renderOtherPart这种要么作为自定义Hook使用,要么作为Component使用,当然就以你举的例子来说更推荐作为Component使用,应为如果作为Hook使用的话renderOtherPart中的state归属于App,作为Component使用时则归属于renderOtherPart,大部分情况下一个层次更深的state始终优于层次更高的,因为更高的层次就代表着需要diff的virtual dom树更大

export default function App() {
  const [random, setRandom] = useState(() => Math.random() * 10);

  const renderOtherPart = function () {
    const [count, setCount] = useState(0);
    return (
      <div>
        {count}
        <button onClick={() => setCount((prev) => prev + 1)}>add2 one</button>
      </div>
    );
  };

  return (
    <div className="App">
      {renderOtherPart()}
    </div>
  );
}
回复
likes
适合作为回答的
  • 经过验证的有效解决办法
  • 自己的经验指引,对解决问题有帮助
  • 遵循 Markdown 语法排版,代码语义正确
不该作为回答的
  • 询问内容细节或回复楼层
  • 与题目无关的内容
  • “赞”“顶”“同问”“看手册”“解决了没”等毫无意义的内容