仅更新一个状态变量时,所有组件都会重新渲染

时间:2020-08-25 08:52:09

标签: javascript reactjs

我有一个页面,该页面显示每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: 在开发工具中,我得到重新渲染的原因是“挂钩已更改”

更新2: 下面是使用此计时器的代码片段。 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;

2 个答案:

答案 0 :(得分:1)

我将首先从依赖项数组中删除elapsed,因为您每秒都会无故运行钩子。然后,我们可能必须查看整个组件,或者将其粘贴到此处,或者使用https://codesandbox.io/之类的东西作为最小的复制示例

答案 1 :(得分:1)

根据评论部分:

因此,看起来渲染中的data.map是元凶。为什么会 导致重新呈现我的列表项?一旦我移动了data.map 到StatusList组件中,而不是将项目作为 props.children,它停止了重新渲染

对于OP,问题在于props.children包装的组件上React.memo的使用。由于仅React.memo会比较道具,React.memo包裹的子组件仍会重新渲染。

请参见下面的示例片段。状态更新仅触发一次重新渲染,这是第一个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>

该问题简要讨论了props.childrenshallowlyReact memo的用法:


默认情况下,如果父级重新渲染,则子级组件也会触发其各自的渲染。 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>

如果您不希望子组件不受父对象的状态更改影响而在子组件上触发渲染,则可以使用https://github.com/facebook/react/issues/14463

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>