Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the 'proper' way to update a react component after an interval with hooks?

I'm using the alpha version of react supporting hooks, and want to validate my approach to updating the text in a component after an interval without rendering the component more times than needed when a prop changes.

EDIT: For clarity - this component is calling moment(timepoint).fromNow() within the formatTimeString function (docs here), so the update isn't totally unneccessary, I promise!

I previously had:

const FromNowString = ({ timePoint, ...rest }) => {
  const [text, setText] = useState(formatTimeString(timePoint));

  useEffect(() => {
    setText(formatTimeString(timePoint));
    let updateInterval = setInterval(
      () => setText(formatTimeString(timePoint)),
      30000
    );
    return () => {
      clearInterval(updateInterval);
    };
  }, [timePoint]);

  // Note the console log here is so we can see when renders occur
  return (
    <StyledText tagName="span" {...rest}>
      {console.log('render') || text}
    </StyledText>
  );
};

This "works" - the component correctly updates if the props change, and the component updates at each interval, however on mounting, and when a prop changes, the component will render twice.

This is because useEffect runs after the render that results when the value of timePoint changes, and inside my useEffect callback I'm immediately calling a setState method which triggers an additional render.

Obviously if I remove that call to setText, the component doesn't appear to change when the prop changes (until the interval runs) because text is still the same.

I finally realised I could trigger a render by setting a state variable that I didn't actually need, like so:

const FromNowString = ({ timePoint, ...rest }) => {
  // We never actually use this state value
  const [, triggerRender] = useState(null);

  useEffect(() => {
    let updateInterval = setInterval(() => triggerRender(), 30000);
    return () => {
      clearInterval(updateInterval);
    };
  }, [timePoint]);

  return (
    <StyledText tagName="span" {...rest}>
      {console.log("render") || formatTimeString(timePoint)}
    </StyledText>
  );
};

This works perfectly, the component only renders once when it mounts, and once whenever the timePoint prop changes, but it feels hacky. Is this the right way of going about things, or is there something I'm missing?

like image 911
Ed_ Avatar asked Jan 25 '19 11:01

Ed_


1 Answers

I think this approach seems fine. The main change I would make is to actually change the value each time, so that it is instead:

const FromNowString = ({ timePoint, ...rest }) => {
  const [, triggerRender] = useState(0);

  useEffect(() => {
    const updateInterval = setInterval(() => triggerRender(prevTriggerIndex => prevTriggerIndex + 1), 30000);
    return () => {
      clearInterval(updateInterval);
    };
  }, [timePoint]);

  return (
    <StyledText tagName="span" {...rest}>
      {console.log("render") || formatTimeString(timePoint)}
    </StyledText>
  );
};

I have two reasons for suggesting this change:

  • I think it will help when debugging and/or verifying the exact behavior that is occurring. You can then look at this state in dev tools and see exactly how many times you have triggered the re-render in this manner.
  • The other reason is just to give people looking at this code more confidence that it will actually do what it is intended to do. Even though setState reliably triggers a re-render (and React is unlikely to change this since it would break too much), it would be reasonable for someone looking at this code to wonder "Does React guarantee a re-render if a setState call doesn't result in any change to the state?" The main reason setState always triggers a re-render even if unchanged is because of the possibility of calling setState after having done mutations to the existing state, but if the existing state is null and nothing is passed in to the setter, that would be a case where React could know that state has not changed since the last render and optimize for it. Rather than force someone to dig into React's exact behavior or worry about whether that behavior could change in the future, I would do an actual change to the state.
like image 173
Ryan Cogswell Avatar answered Oct 16 '22 08:10

Ryan Cogswell