2

I have a page which displays some data retrieved every 180 seconds (3 minutes) from an API. When the page loads I have an initial useEffect hook to call the retrieveData() function. Then, using the below code and a state variable called elapsed, I start an interval for the 180 seconds. Every one second, I update a progress bar which is counting down from 3 minutes and displays the remaining time. This uses the elapsed state variable.

This is the hook to start the interval:

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

My question is, why are the rest of the components on the page being re-rendered on each tick of the interval? I thought that since I'm only updating the elapsed state variable, only those component will update that use that state variable. My table which is displaying the data (stored in another state variable) should only update every 3 minutes.

Happy to provide other info if this is not enough to go off of.

UPDATE 1: In the dev tools, the reason I get for the re-render is "Hooks changed"

UPDATE 2: Below is a snippet of the code where this timer is used. The StatusList and StatusListItem components use React.memo and are functional components. The status list is an IonList and the StatusListItem is an IonItem at top level (from 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;
bluiska
  • 369
  • 2
  • 12

2 Answers2

1

I would start with removing the elapsed from dependency array, as you run the hook every second, again for no reason. Then we would probably have to see the whole component, either paste it here, or use something like https://codesandbox.io/ for minimal repro example

Lukáš Gibo Vaic
  • 3,960
  • 1
  • 18
  • 30
  • I have removed elapsed and that only triggers the interval once, no updating the components after the first trigger. – bluiska Aug 25 '20 at 09:00
  • https://upmostly.com/tutorials/setinterval-in-react-components-using-hooks you basicaly dont need interval in that case, because you run it once and then delete it, if it doesnt update, your clear function is being called which is another separate problem. – Lukáš Gibo Vaic Aug 25 '20 at 09:02
  • Yeah, elapsed is not updated like that @Kalhan.Toress. – bluiska Aug 25 '20 at 09:05
  • @LukášGiboVaic I have previously tried running the above code with a setTimeout and keeping elapsed in the dependency array which in effect, creates a loop of updating the effect every one second (like an interval). My question is, why are all the components updating when that changes? Does react work in such a way that all the components re-render if ANY of the state changes? – bluiska Aug 25 '20 at 09:07
  • I have updated the original post with my simplified code snippet. – bluiska Aug 25 '20 at 09:12
  • Could aphrodite cause the re-render? Changing of styling? – bluiska Aug 25 '20 at 09:19
  • So it looks like that data.map in the render is the culprit. Why would that cause a re-render of my list items? Once I moved the data.map into the StatusList component instead of passing the items in as props.children, it stopped re-rendering – bluiska Aug 26 '20 at 16:29
1

According to the comments section:

So it looks like that data.map in the render is the culprit. Why would that cause a re-render of my list items? Once I moved the data.map into the StatusList component instead of passing the items in as props.children, it stopped re-rendering

In the case of the OP, the issue is with regards to the usage of props.children on a React.memo wrapped component. Since the React.memo only shallowly compares props, the React.memo wrapped child component will still re-render.

See my example snippet below. Only 1 rerender will trigger on update of state and that is the 1st ChildComponent which is passed an object as children prop.

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>

Issue that briefly discusses usage of props.children in a React memo: https://github.com/facebook/react/issues/14463


By default if the parent re-renders, the child components will trigger their respective render as well. The difference in React is: although in this scenario the re-render occurs for the child component, the DOM is not updated. The re-rendering is evident in the following example where the ChildComponent does not even have a prop and therefore does not use the parent's state:

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>

If you do not want renders to get triggered on the Child components if they are unaffacted by the state changes of the parent, you can use 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>
95faf8e76605e973
  • 13,643
  • 3
  • 24
  • 51
  • Thank you for that. This is a good answer but what I am unsure about is why in my case I get an update on the components where props do not change. I have added React memo to every single component of mine but it is still re-rendering the data fields every second when it should only re-render the progress bar. The data should only change once every 3 minutes. – bluiska Aug 25 '20 at 10:46
  • that should not be the case, are you able to reproduce the issue in codesandbox? perhaps there is an external influence (not provided in your example) that is causing the re-renders to occur – 95faf8e76605e973 Aug 25 '20 at 11:43
  • I'll try if I get a chance, it very well could be external. – bluiska Aug 25 '20 at 12:28
  • So it looks like that data.map in the render is the culprit. Why would that cause a re-render of my list items? Once I moved the data.map into the StatusList component instead of passing the items in as props.children, it stopped re-rendering – bluiska Aug 26 '20 at 16:29
  • You are correct. Each time the parent render is called, you are passing newly created react elements (JSX) - this is why the prop `children` is considered as "new" therefore re-renders the children - they are not memoized as you'd expect. See this [issue](https://github.com/facebook/react/issues/14463)'s 2nd comment. It briefly but specifically mentions usage of `props.children` when using `React.memo` – 95faf8e76605e973 Aug 26 '20 at 21:11
  • Well I got to the bottom of the issue, thanks a lot for your help. Question is now, how can I change the architecture around? Currently i have a inside of which I render StatusListItemNumbered or StatusListItemSuccess. The List is generic and the items can change. If I only pass the data and map through it inside the component, how could I decide exactly which items I want to render? What if I wanted one numbered and one success list item in the same StatusList? – bluiska Aug 27 '20 at 08:56
  • I just figured it out! I created a const variable that loops through my state variable and creates a list of ListItem components. This whole thing is wrapped in a useMemo with the state variable as its dependency. With this architecture, I can keep the props.children and the List or the items, no longer re-render!! – bluiska Aug 27 '20 at 09:08
  • That's great - perhaps you can share your solution with us if you have spare time @bluiska – 95faf8e76605e973 Aug 27 '20 at 09:59
  • I will certainly when I get a chance to type it up. – bluiska Aug 27 '20 at 13:47