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