Reputation: 4439
I tried to make a nice animating example for myself using Hooks but I have stumbled upon a problem where my functions won't have the updated version of a state var and continue to use the first version.
In the snippet below I have an example where once you click on a bar it's supposed to move in a square formation. First going east, then south, then west, then north and then east again etc. However it never goes south because even though its direction is updated from north
to east
(indicated by the text on the bar or clicking on it again), the functions still think that the direction is north.
const Example = (props) => {
const [ direction, setDirection ] = React.useState('North');
console.log("Rendering Example: ", direction);
const isAnimating = React.useRef(false)
const blockRef = React.useRef(null);
// Animate on click.
const onClick = () => {
if (!isAnimating.current) {
decideDirection();
isAnimating.current = true
} else {
console.log("Already animating. Going: ", direction);
}
};
const decideDirection = () => {
console.log("Current direction: ", direction);
if (direction === 'North') {
moveEast();
} else if (direction === 'East') {
moveSouth();
} else if (direction === 'South') {
moveWest();
} else if (direction === 'West') {
moveNorth();
}
};
const move = (toX, toY, duration, onComplete) => {
Velocity(blockRef.current, {
translateX: toX,
translateY: toY,
complete: () => {
onComplete();
}
},
{
duration: duration
});
}
const moveNorth = () => {
setDirection('North');
console.log('Moving N: ', direction);
move(0, 0, 500, () => {
decideDirection();
})
}
const moveEast = () => {
setDirection('East');
console.log('Moving E: ', direction);
move(500, 0, 2500, () => {
decideDirection();
})
};
const moveSouth = () => {
setDirection('South');
console.log('Moving S: ', direction);
move(500, 18, 500, () => {
decideDirection();
})
}
const moveWest = () => {
setDirection('West');
console.log('Moving W: ', direction);
move(0, 18, 2500, () => {
decideDirection();
})
}
return(
<div>
<div id='block' onClick={onClick} ref={blockRef} style={{ width: '100px', height: '18px', backgroundColor: 'red', textAlign: 'center'}}>{direction}</div>
</div>
);
};
ReactDOM.render(<div><Example/></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>
I find this kind of strange because none of these functions are memoized so they should re-create every render and thus have the new value. Even if I do add something like useCallback
and provide direction
to every function it still won't work. Why don't the functions know about the updated version of the state var?
Upvotes: 7
Views: 6936
Reputation: 81016
The issue is that your move
functions have closed over the initial version of decideDirection
. Your div has re-rendered, but the animation continues with references to the initial versions of the functions. One way to resolve this is to use a ref to point at the decideDirection
function (I've shown this approach in the snippet below).
This seems a little brittle though since the timing of the re-render vs. the timing of the animation makes it difficult to reason as to whether this is robust. A more robust approach would involve chaining the moves more explicitly via state changes such that rather than directly calling decideDirection
at completion of each animation, you instead set state that will trigger an effect to start the next animation.
const Example = (props) => {
const [ direction, setDirection ] = React.useState('North');
console.log("Rendering Example: ", direction);
const isAnimating = React.useRef(false)
const blockRef = React.useRef(null);
const decideDirection = () => {
console.log("Current direction: ", direction);
if (direction === 'North') {
moveEast();
} else if (direction === 'East') {
moveSouth();
} else if (direction === 'South') {
moveWest();
} else if (direction === 'West') {
moveNorth();
}
};
const decideDirectionRef = React.useRef(decideDirection);
React.useEffect(()=>{
decideDirectionRef.current = decideDirection;
});
// Animate on click.
const onClick = () => {
if (!isAnimating.current) {
decideDirectionRef.current();
isAnimating.current = true
} else {
console.log("Already animating. Going: ", direction);
}
};
const move = (toX, toY, duration, onComplete) => {
Velocity(blockRef.current, {
translateX: toX,
translateY: toY,
complete: () => {
onComplete();
}
},
{
duration: duration
});
}
const moveNorth = () => {
setDirection('North');
console.log('Moving N: ', direction);
move(0, 0, 500, () => {
decideDirectionRef.current();
})
}
const moveEast = () => {
setDirection('East');
console.log('Moving E: ', direction);
move(500, 0, 2500, () => {
decideDirectionRef.current();
})
};
const moveSouth = () => {
setDirection('South');
console.log('Moving S: ', direction);
move(500, 18, 500, () => {
decideDirectionRef.current();
})
}
const moveWest = () => {
setDirection('West');
console.log('Moving W: ', direction);
move(0, 18, 2500, () => {
decideDirectionRef.current();
})
}
return(
<div>
<div id='block' onClick={onClick} ref={blockRef} style={{ width: '100px', height: '18px', backgroundColor: 'red', textAlign: 'center'}}>{direction}</div>
</div>
);
};
ReactDOM.render(<div><Example/></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>
Upvotes: 5