最近遇到一个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());}
在线复现代码地址:

通过运行代能够发现,以 <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。

const RenderPart = function () {  const [state, setState] = useState(() => {    console.log("RenderPart initial state");    return 0;  });  // return CHild();  console.log("renderPart RenderPart");  return (    <div>      <h3>{state}</h3>      <button onClick={() => setState((prev) => prev + 1)}>setstate</button>      <CHild></CHild>    </div>  );};export default function App() {     const [random, setRandom] = useState(() => Math.random() * 10);    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>        </div>      );}

或者应用 useCallBack :

function App() {  const [random, setRandom] = useState(() => Math.random() * 10);  const RenderPart = useCallback(function () {    return <CHild></CHild>;  }, []);  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>    </div>  );}