【问题标题】:All components re-rendering when only a single state variable is updated仅更新单个状态变量时所有组件重新渲染
【发布时间】:2020-08-25 08:52:09
【问题描述】:

我有一个页面显示每 180 秒(3 分钟)从 API 检索的一些数据。当页面加载时,我有一个初始的 useEffect 挂钩来调用 retrieveData() 函数。然后,使用下面的代码和一个名为 elapsed 的状态变量,我开始了 180 秒的间隔。每隔一秒,我会更新一个从 3 分钟开始倒计时的进度条,并显示剩余时间。这使用经过的状态变量。

这是开始间隔的钩子:

useEffect(() => {
    const interval = setInterval(() => {
      if (elapsed < dataUpdateInterval) {
        setElapsed(elapsed + 1);
      } else {
        setElapsed(0);
        retrieveData();
      }
    }, 1000);
    return () => clearInterval(interval);
  }, [elapsed]);

我的问题是,为什么页面上的其余组件会在间隔的每个刻度上重新呈现?我认为由于我只更新经过的状态变量,因此只有那些组件会更新使用该状态变量的组件。我的表格显示数据(存储在另一个状态变量中)应该每 3 分钟更新一次。

如果这还不够,很乐意提供其他信息。

更新 1: 在开发工具中,我得到重新渲染的原因是“Hooks changed”

更新 2: 下面是使用此计时器的代码的 sn-p。 StatusList 和 StatusListItem 组件使用 React.memo 并且是功能组件。状态列表是一个 IonList,而 StatusListItem 是一个顶级的 IonItem(来自 Ionic)

import React, { useEffect, useState } from "react";
import { Layout } from "antd";
import StatusListItemNumbered from "./StatusListItemNumbered";
import { Container, Row, Col } from "react-bootstrap";
import StatusList from "./StatusList";
import { Progress } from "antd";
import moment from "moment";

const Content = React.memo(() => {
  const dataUpdateInterval = 180;

  const [elapsed, setElapsed] = useState(0);
  const [data, setData] = useState();

  const timeLeft = (interval, elapsed) => {
    let time = moment.duration(interval - elapsed, "seconds");
    return moment.utc(time.asMilliseconds()).format("mm:ss");
  };

  const retrieveData = () => {
    console.log("Retrieving data");
    SomeApi.getData().then(items => {
        setData(items);
    })
  };

  //The effect hook responsible for the timer
  useEffect(() => {
    setTimeout(() => {
      if (elapsed < dataUpdateInterval) {
        setElapsed(elapsed + 1);
      } else {
        setElapsed(0);
        retrieveData();
      }
    }, 1000);
  }, [elapsed]);

  //Retrieve data the very first time the component loads.
  useEffect(() => {
    retrieveData();
  }, []);

  //Component styling
  return (
    <Layout.Content style={{ padding: "20px 20px" }}>
      <div className={css(styles.siteLayoutContent)}>
        <Container className={css(styles.mainContainer)}>
          <Row className={css(styles.progressRow)}>
            <Col>
              <Progress
                style={{ marginLeft: "17px", width: "99%" }}
                strokeColor={{
                  "0%": "#108ee9",
                  "100%": "#87d068",
                }}
                percent={(100 / dataUpdateInterval) * elapsed}
                format={() => `${timeLeft(dataUpdateInterval, elapsed)}`}
              />
            </Col>
          </Row>
          <Row>
            <Col sm={6}>
              <StatusList listName="Data">
                {data &&
                  data.map((item, index) => {
                    return (
                      <StatusListItemNumbered
                        key={index}
                        value={item.count}
                        label={item.company}
                      />
                    );
                  })}
              </StatusList>
            </Col>
          </Row>
        </Container>
      </div>
    </Layout.Content>
  );
});

export default Content;

【问题讨论】:

    标签: javascript reactjs


    【解决方案1】:

    根据 cmets 部分:

    所以看起来渲染中的 data.map 是罪魁祸首。为什么会 导致我的列表项重新呈现?一旦我移动了 data.map 进入 StatusList 组件,而不是将项目作为 props.children,它停止重新渲染

    对于 OP,问题在于 props.childrenReact.memo 包装组件上的使用。由于React.memo 只有shallowly 比较props,所以React.memo 包装的子组件仍将重新渲染。

    请参阅下面的示例 sn-p。只有 1 个重新渲染会在状态更新时触发,即第一个 ChildComponent,它作为 children 属性传递一个对象。

    let number_of_renders = 0;
    
    const ChildComponent = React.memo((props) => {
      number_of_renders++;
      React.useEffect(()=>{
        console.log("ChildComponent renders: " + number_of_renders)
      })
      return (
        <div></div>
      )
    })
    
    const App = () => {
      const [obj, setObj] = React.useState({ key: "tree", val: "narra" });
      
      const [primitiveData, setPrimitiveData] = React.useState(0)
      
      return (
        <React.Fragment>
          <button onClick={()=>setObj({ key: "tree", val: "narra" })}>Update Object</button>
          <button onClick={()=>setPrimitiveData(primitiveData + 1)}>Update Primitive Data</button>
          <ChildComponent>
            {obj}
          </ChildComponent>
          <ChildComponent>
            {obj.val}
          </ChildComponent>
          <ChildComponent>
            primitiveData
          </ChildComponent>
        </React.Fragment>
      )
    }
    
    ReactDOM.render(<App/>, document.getElementById("root"))
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
    <div id="root"></div>

    React memo 中简要讨论props.children 用法的问题:https://github.com/facebook/react/issues/14463


    默认情况下,如果父组件重新渲染,子组件也会触发它们各自的渲染。 React 的不同之处在于:虽然在这种情况下子组件发生了重新渲染,但 DOM 并没有更新。重新渲染在以下示例中很明显,其中 ChildComponent 甚至没有道具,因此不使用父级的状态:

    const ChildComponent = () => {
      React.useEffect(()=>{
        console.log("ChildComponent rerender")
      })
      return (
        <div>Child Component</div>
      )
    }
    
    const App = () => {
      const [state, setState] = React.useState(true);
      
      return (
        <React.Fragment>
          <button onClick={()=>setState(!state)}>Update State</button>
          <ChildComponent/>
        </React.Fragment>
      )
    }
    
    ReactDOM.render(<App/>, document.getElementById("root"))
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
    <div id="root"></div>

    如果您不希望在子组件不受父组件状态更改影响的情况下触发渲染,您可以使用React.memo

    const ChildComponent = React.memo((props) => {
      React.useEffect(()=>{
        console.log("ChildComponent rerender")
      })
      return (
        <div>Child Component</div>
      )
    })
    
    const TheStateDependentChild = React.memo((props) => {
      React.useEffect(()=>{
        console.log("TheStateDependentChild rerender")
      })
      return (
        <div>TheStateDependentChild Component</div>
      )
    })
    
    const App = () => {
      const [state, setState] = React.useState(true);
      
      return (
        <React.Fragment>
          <button onClick={()=>setState(!state)}>Update State</button>
          <ChildComponent/>
          <TheStateDependentChild sampleProp={state}/>
        </React.Fragment>
      )
    }
    
    ReactDOM.render(<App/>, document.getElementById("root"))
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
    <div id="root"></div>

    【讨论】:

    • 谢谢你。这是一个很好的答案,但我不确定的是,为什么在我的情况下,我会得到关于 props 不变的组件的更新。我已经为我的每个组件添加了 React 备忘录,但它仍然每秒重新渲染数据字段,而它应该只重新渲染进度条。数据应每 3 分钟更改一次。
    • 那不应该是这样,你能在codesandbox中重现这个问题吗?可能存在导致重新渲染发生的外部影响(您的示例中未提供)
    • 如果有机会我会试试的,很可能是外部的。
    • 所以看起来渲染中的 data.map 是罪魁祸首。为什么这会导致我的列表项重新呈现?一旦我将 data.map 移动到 StatusList 组件中,而不是将项目作为 props.children 传递,它就会停止重新渲染
    • 你是对的。每次调用父渲染时,您都在传递新创建的反应元素 (JSX) - 这就是为什么 prop children 被认为是“新的”,因此会重新渲染子渲染 - 它们不会像您期望的那样被记忆。请参阅此issue 的第二条评论。在使用React.memo时,它简要但特别提到了props.children的用法
    【解决方案2】:

    我会先从依赖数组中删除elapsed,因为你每秒运行一次钩子,再次无缘无故。然后我们可能需要查看整个组件,要么将其粘贴到此处,要么使用 https://codesandbox.io/ 之类的内容作为最小重现示例

    【讨论】:

    • 我已经删除了 elapsed 并且只触发了一次间隔,第一次触发后没有更新组件。
    • upmostly.com/tutorials/… 在这种情况下你基本上不需要间隔,因为你运行它一次然后删除它,如果它没有更新,你的 clear 函数被调用,这是另一个单独的问题。
    • 是的,elapsed 不会像 @Kalhan.Toress 那样更新。
    • @LukášGiboVaic 我之前曾尝试使用 setTimeout 运行上述代码并在依赖数组中保持 elapsed,这实际上创建了一个每隔一秒更新一次效果的循环(如间隔)。我的问题是,为什么所有组件都会在更改时更新?如果任何状态发生变化,react 是否会以所有组件重新呈现的方式工作?
    • 我已经用我的简化代码 sn-p 更新了原始帖子。
    猜你喜欢
    • 2018-06-05
    • 2023-03-09
    • 2021-10-15
    • 2020-10-24
    • 1970-01-01
    • 2019-11-09
    • 2021-05-24
    • 2020-09-30
    • 1970-01-01
    相关资源
    最近更新 更多