Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React hooks function component prevent re-render on state update

I'm learning about React Hooks which means I'm going to have to move away from classes to function components. Previously in classes I could have class variables independent of the state that I could update without the component re-rendering. Now that I am attempting to re-create a component as a function component with hooks I have ran into the problem that I can't (as far as I know) make variables for that function so the only way to store data is through the useState hook. However this means my component will re-render whenever that state is updated.

I've illustrated it in the example below where I attempted to re-create a class component as a function component that uses hooks. I want to animate a div if someone clicks on it, but prevent the animation from being called again if the user clicks while it's already animating.

class ClassExample extends React.Component {
  _isAnimating = false;
  _blockRef = null;
  
  onBlockRef = (ref) => {
    if (ref) {
      this._blockRef = ref;
    }
  }
  
  // Animate the block.
  onClick = () => {
    if (this._isAnimating) {
      return;
    }

    this._isAnimating = true;
    Velocity(this._blockRef, {
      translateX: 500,
      complete: () => {
        Velocity(this._blockRef, {
          translateX: 0,
          complete: () => {
            this._isAnimating = false;
          }
        },
        {
          duration: 1000
        });
      }
    },
    {
      duration: 1000
    });
  };
  
  render() {
    console.log("Rendering ClassExample");
    return(
      <div>
        <div id='block' onClick={this.onClick} ref={this.onBlockRef} style={{ width: '100px', height: '10px', backgroundColor: 'pink'}}>{}</div>
      </div>
    );
  }
}

const FunctionExample = (props) => {
  console.log("Rendering FunctionExample");
  
  const [ isAnimating, setIsAnimating ] = React.useState(false);
  const blockRef = React.useRef(null);
  
  // Animate the block.
  const onClick = React.useCallback(() => {
    if (isAnimating) {
      return;
    }

    setIsAnimating(true);
    Velocity(blockRef.current, {
      translateX: 500,
      complete: () => {
        Velocity(blockRef.current, {
          translateX: 0,
          complete: () => {
            setIsAnimating(false);
          }
        },
        {
          duration: 1000
        });
      }
    },
    {
      duration: 1000
    });
  });
  
  return(
    <div>
      <div id='block' onClick={onClick} ref={blockRef} style={{ width: '100px', height: '10px', backgroundColor: 'red'}}>{}</div>
    </div>
  );
};

ReactDOM.render(<div><ClassExample/><FunctionExample/></div>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.2/velocity.min.js"></script>

<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>

<div id='root' style='width: 100%; height: 100%'>
</div>

If you click on the ClassExample bar(pink) you will see that it does not re-render while animating, however if you click on the FunctionExample bar(red) it will rerender twice while it is animating. This is because I'm using setIsAnimating which causes the re-render. I know it's probably not very performance-winning but I would like to prevent it if it's at all possible with a function component. Any suggestions/am I doing something wrong?

Update (attempted fix, no solution yet): Below user lecstor suggested possibly changing the result of useState to let instead of const and then setting it directly let [isAnimating] = React.useState(false);. This unfortunately does not work either as you can see in the snippet below. Clicking on the red bar will start its animation, clicking on the orange square will make the component re-render, and if you click on the red bar again it will print that isAnimating is reset to false even though the bar is still animating.

const FunctionExample = () => {
  console.log("Rendering FunctionExample");

  // let isAnimating = false; // no good if component rerenders during animation
  
  // abuse useState var instead?
  let [isAnimating] = React.useState(false);
  
  // Var to force a re-render.
  const [ forceCount, forceUpdate ] = React.useState(0);

  const blockRef = React.useRef(null);

  // Animate the block.
  const onClick = React.useCallback(() => {
    console.log("Is animating: ", isAnimating);
    if (isAnimating) {
      return;
    }
    
    isAnimating = true;
    Velocity(blockRef.current, {
      translateX: 500,
      complete: () => {
        Velocity(blockRef.current, {
          translateX: 0,
          complete: () => {
            isAnimating = false;
          }
        }, {
          duration: 5000
        });
      }
    }, {
      duration: 5000
    });
  });

  return (
  <div>
    <div
      id = 'block'
      onClick = {onClick}
      ref = {blockRef}
      style = {
        {
          width: '100px',
          height: '10px',
          backgroundColor: 'red'
        }
      }
      >
      {}
    </div>
    <div onClick={() => forceUpdate(forceCount + 1)} 
      style = {
        {
          width: '100px',
          height: '100px',
          marginTop: '12px',
          backgroundColor: 'orange'
        }
      }/>
  </div>
  );
};

ReactDOM.render( < div > < FunctionExample / > < /div>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.2/velocity.min.js"></script>

<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>

<div id='root' style='width: 100%; height: 100%'>
</div>

Update 2(solution): If you want to have a variable in a function component but not have it re-render the component when it's updated, you can use useRef instead of useState. useRef can be used for more than just dom elements and is actually suggested to be used for instance variables. See: https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables

like image 994
ApplePearPerson Avatar asked Mar 06 '19 11:03

ApplePearPerson


People also ask

How do you prevent re-render in React functional component?

If the child component is re-rendered without any change in its props then it could be prevented by using hooks. React.

How do I stop rendering in React hooks?

f you're using a React class component you can use the shouldComponentUpdate method or a React. PureComponent class extension to prevent a component from re-rendering. Use React. memo() to prevent re-rendering on React function components.

How do you prevent a child component from re-rendering React?

OR, we can remove function memoization here, and just wrap ChildComponent in React. memo : MovingComponent will re-render, “children” function will be triggered, but its result will be memoized, so ChildComponent will never re-render.


2 Answers

Use a ref to keep values between function invocations without triggering a render

class ClassExample extends React.Component {
  _isAnimating = false;
  _blockRef = null;
  
  onBlockRef = (ref) => {
    if (ref) {
      this._blockRef = ref;
    }
  }
  
  // Animate the block.
  onClick = () => {
    if (this._isAnimating) {
      return;
    }

    this._isAnimating = true;
    Velocity(this._blockRef, {
      translateX: 500,
      complete: () => {
        Velocity(this._blockRef, {
          translateX: 0,
          complete: () => {
            this._isAnimating = false;
          }
        },
        {
          duration: 1000
        });
      }
    },
    {
      duration: 1000
    });
  };
  
  render() {
    console.log("Rendering ClassExample");
    return(
      <div>
        <div id='block' onClick={this.onClick} ref={this.onBlockRef} style={{ width: '100px', height: '10px', backgroundColor: 'pink'}}>{}</div>
      </div>
    );
  }
}

const FunctionExample = (props) => {
  console.log("Rendering FunctionExample");
  
  const isAnimating = React.useRef(false)
  const blockRef = React.useRef(null);
  
  // Animate the block.
  const onClick = React.useCallback(() => {
    if (isAnimating.current) {
      return;
    }

    isAnimating.current = true
    
    Velocity(blockRef.current, {
      translateX: 500,
      complete: () => {
        Velocity(blockRef.current, {
          translateX: 0,
          complete: () => {
            isAnimating.current = false
          }
        },
        {
          duration: 1000
        });
      }
    },
    {
      duration: 1000
    });
  });
  
  return(
    <div>
      <div id='block' onClick={onClick} ref={blockRef} style={{ width: '100px', height: '10px', backgroundColor: 'red'}}>{}</div>
    </div>
  );
};

ReactDOM.render(<div><ClassExample/><FunctionExample/></div>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.2/velocity.min.js"></script>

<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>

<div id='root' style='width: 100%; height: 100%'>
</div>
like image 170
thedude Avatar answered Oct 09 '22 00:10

thedude


Why is it that you think you can't use internal variables in the same way as you do with classes?

ok, feels a bit dirty but how about mutating the useState state? 8)

nope, that doesn't work as intended state is reset on re-render

So as this is not related to the actual rendering of the component, maybe our logic needs to be based on the animation itself. This particular problem can be resolved by checking the class that velocity sets on the element while it is being animated.

const FunctionExample = ({ count }) => {
  console.log("Rendering FunctionExample", count);

  // let isAnimating = false; // no good if component rerenders during animation
  
  // abuse useState var instead?
  // let [isAnimating] = React.useState(false);
  
  const blockRef = React.useRef(null);

  // Animate the block.
  const onClick = React.useCallback(() => {
    // use feature of the anim itself
    if (/velocity-animating/.test(blockRef.current.className)) {
      return;
    }
    console.log("animation triggered");
    
    Velocity(blockRef.current, {
      translateX: 500,
      complete: () => {
        Velocity(blockRef.current, {
          translateX: 0,
        }, {
          duration: 1000
        });
      }
    }, {
      duration: 5000
    });
  });

  return (
  <div>
    <div
      id = 'block'
      onClick = {onClick}
      ref = {blockRef}
      style = {
        {
          width: '100px',
          height: '10px',
          backgroundColor: 'red'
        }
      }
      >
      {}
    </div>
  </div>
  );
};

const Counter = () => {
  const [count, setCount] = React.useState(0);
  return <div>
    <FunctionExample count={count} />
    <button onClick={() => setCount(c => c + 1)}>Count</button>
  </div>;
}

ReactDOM.render( < div > < Counter / > < /div>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.2/velocity.min.js"></script>

<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>

<div id='root' style='width: 100%; height: 100%'>
</div>
like image 1
lecstor Avatar answered Oct 08 '22 23:10

lecstor