Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid setState() inside render() when state depends on render

Tags:

reactjs

I have a grid that renders cards of variable heights.

To get the height of the cards, I render the card wrapped in ReactHeight (https://github.com/nkbt/react-height), which lets me pass a callback function to it when the height is ready.

I keep the card heights in an array in the component state, which I update as card heights get reported.

When all heights are available, I calculate the padding from the heights, which render passes to the cards

In a simplified version, it looks like this.

class Grid extends Component {
  constructor() {
    ...
    this.state = { cardHeights: {}, paddings: [] }; //initial state
  }

  componentDidUpdate() {
    const i = setInterval(() => {
      if (all heights are ready) {
        // ...calculate paddings
        this.setState(paddings: calculatedPaddings);

        clearInterval(i);
      }
    }, 100);
    // *A*
  }

  updateCardHeights(index, height) {
    this.setState(prevState => ({
      cardHeights: {
        ...prevState.cardHeights,
        [index]: height,
      },
    }));
  }

  render() {
    // index = cards are indexed like 1   2  3
    //                                4   5  6
    //                                7   8  9
    //                                10 11 12
    //                                     ...
    return (
      <ReactHeight onHeightReady={ height => this.updateCardHeights(index, height) }
         <Card padding={this.state.paddings[index]}/>
      </ReactHeight>
    );

  }
}

I know setState() calls render, and calling a function mutating the state inside render generally leads to an infinite loop.

In my code, render calls a function that updates this.state.cardHeights array, and the rendering of its child component, Card, indirectly depends on this.state.cardHeights, since this.state.paddings that it directly depends on is calculated based on cardHeights on componentDidUpdate().

However, I cannot get the height of the Card component without rendering it, so the only way I can think of is making padding a prop to card and state of the component, and then changing the state after rendering.

My question is:

  1. Somehow my code didn't result in an infinite loop even though I'm changing state inside render, but is a better way to do this?

  2. clearInterval() inside my componentDidUpdate() function doesn't seem to work. I put a print statement in *A* part, and it keeps printing even when the if condition is met and clearInterval() is supposedly called. Can this be because of my bad state mutation?

like image 498
Eric Na Avatar asked May 18 '17 02:05

Eric Na


People also ask

What happens when you call setState () inside render () method?

Because setState() function changes the state of the application and causing a change in the state called the render() function again. So if you write something like this then calling the function stack will go for infinity and application gets the crash.

Why putting setState () in render () is not preferred?

The render() function should be pure, meaning that it does not modify a component's state. It returns the same result each time it's invoked, and it does not directly interact with the browser. In this case, avoid using setState() here.

How do I stop render setState?

The most straightforward approach to preventing state update re-renders is by avoiding the state update entirely. This means making sure that the setState function is not called unless strictly necessary.

Can we update state without using setState?

In other words, if we update state with plain JavaScript and not setState , it will not trigger a re-render and React will not display those (invalid) changes in state to our user. This is a simple, but crucial lesson to remember.


1 Answers

In such case you should check if the previous value and the new value are same before updating the state. I hope your height does not gets updated for each render. So you can have a check inupdateCardheights to determine if there is an actual change and then update the state. This ensures that it does not updates the state for redundant updates. But if on each render your state is going to be updated, then the problem would remain the same.

updateCardHeights(index, height) {
 if(this.state.cardHeights[index] !== height) {
  this.setState(prevState => ({
   cardHeights: {
     ...prevState.cardHeights,
     [index]: height,
   },
  }));
 }
}
like image 171
Panther Avatar answered Sep 18 '22 09:09

Panther