博客 / 詳情

返回

在 hook 組件中拆分時圖時需要注意的事項

最近遇到一個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());
}
在線復現代碼地址:Edit wonderful-mccarthy-r2llos

通過運行代可以發現,以 <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>
  );
}
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.