Last week, at work, I got a task to make a confetti celebration. The designer showed me this site and asked me to make a confetti like this. The idea is to show the celebration for some time when the user levels up.
I had never designed a confetti before. I googled a bit and found some react libraries for the same (my project is in react). Then I thought to myself how hard can it be to design one. I have found a vanilla JS implementation for the same here. I just had to wrap this logic inside a react component and put the same in a common place to consume it later. This had two advantages. First, the libraries are more generalized. So they will put a few unnecessary lines of code in my project. I wanted to avoid that. Secondly, I thought it would be a good learning experience.
You can find the repository for the code here.
Before we begin coding, let’s set our goals.
- We want to design a confetti like in the video above.
- We should be able to control properties like number of particles, wind speed, gravity, colors array of particles from parent through prop.
- I should be able to start and stop my animation from the parent.
- We should be able to load multiple instances of the animation inside a single parent.
I set up my project using create-react-app. I downgraded the react and react-dom version from 17.0.0 to 16.11.0 as my current project also has a react 16.11.0 set up (though it was not needed considering react 17 blog). Then I deleted all the unnecessary files. Below is a screenshot of my workspace. You can directly checkout to this commit.
We will use App.js as our parent component and will create a confetti folder for the animated component.
While coding I don’t want a part of my brain to be concerned about formatting. So I use prettier. You can see some git diffs related to formatting in this commit. It is always better to set up prettier before you start a project. (commit)
Disclaimer: I got lazy while writing this article and used screenshots instead of code blocks. But the entire article is divided into seven stages. And the commit link is given at the beginning of each section. So you won’t have much difficulty following up.
Stage 1: (commit)
I have wrapped the vanilla JS code in a react component. The confetti component is using HTML canvas to to surface the animation.
Let me explain what all these methods inside our component do.
useEffect: This is a pretty common react hook. We have provided the dependency list with an empty array. So the call back will trigger once the component mounts. In the call back we are setting up the canvas context, width and height. There is a handler which will reset the canvas dimensions to avoid the animation being pixelated in devices with more pixel ratios. In case of any animation timeout props provided by the parent or the component unmount the cancelAnimationFrame method is triggered to cancel the animation frame request.
startAnimation: This method will be triggered from useEffect callback once the component mounts. This will set the particles array with particles of different shape, color and position.
setAnimation: Forms random particles.
runAnimation: Will be called with each frame refresh.
updateAndDrawParticles: Responsible for the movement of particles on the canvas. Will update the coordinate position and tilt of the particles on each call.
The canvas has its height and width set to ‘100%’ but we can override the styles from the parent. On mounting of the component, the animation will start. On each window.requestAnimationFrame, the runAnimation method will be called, which in turn will invoke the updateAndDrawParticles method, which will make the particles move on the canvas. On timeout (props passed from parent) or unmount of the component, we will cancel the window.cancelAnimationFrame method to end the infinite call back loop of runAnimation.
Now that we have our basic setup we should be able to Start and stop the animation from the parent.
Stage 2: (commit)
Let’s create two buttons in our parent code, ‘start’ and ‘stop’ respectively. We will control the confetti from the parent through these two buttons.
On click of start and stop button the playAnimation prop will change. We should include playAnimation as a dependency in our useEffect hook. Our confetti component will now rerender every time playAnimation prop changes.
When you hit the ‘stop’ button, window.requestAnimationFrame will be cancelled and the particles will stay where they were. We don’t want that, we want our particles to fall smoothly.
Stage 3: (commit)
How do we achieve a smooth stopping animation? Simple. Instead of cancelling the window.requestAnimationFrame, we will remove the particle which moves away from the canvas view area from the particles array. Let’s modify our updateAndDrawParticles method to achieve that.
If you are running the code, you will observe, though a smooth stop animation is achieved, it is not in continuation with the previous animation. Why is that?
The animation is running, so playAnimation is true. If we hit the stop button playAnimation will be false in App.js. And because we have playAnimation as a dependency in the useEffect hook of the confetti component, it will rerender. Remember we had declared our particles array at the top as const particles = . This will reset. useEffect callback will run which triggers startAnimation, which will then call setParticle to fill the particles array with random colored-positioned particles. In summary, our updateAndDrawParticles will get a new particles array to perform stop-animation.
We need to find a way to persist our particles array on props change.
Stage 4 (commit)
How do we persist particles array on component rerender?
- We will use the useState hook and bring the array into confetti state. But there is a caveat. Every time we reset the particles array, the confetti component will re-render and I will lose other variables like animationId, waveAngle, count etc. You can now argue what if we move all of them to state? Then your component will re-render for every single update and it will be a huge performance overhead. Just to give an idea, take waveAngle for example. For a particle count of 200, this component will re-render 60 * 200 = 1200 times considering a 60fps device.
- We need to expose two methods startAnimation (already existing) and stopAnimation to the parent. The parent component should be able to call each of them accordingly. But there is also a problem here. Child to parent communication is not react’s forte.
I will be using react-refs to try and solve the second point. You can read more about react-refs here.
I need start and stop functionalities contained inside the confetti component and on trigger of them component should not re-render and retain all its local variables declared with let and const. And also on component unmount the animation frame requests should be cancelled.
The callback function inside useEffect returns another function. This function will trigger during component unmount and the animation frame request will be cancelled. The stopAnimation function only toggles the local variable stopStremingConfetti, which is moved from local scope of updateAndDrawParticles method to the scope of the whole component. startAnimation method is modified to initialize the canvas.
[Task-1] The first line of startAnimation method cancels the animation frame request. Comment this line and then click start and stop buttons multiple times. See what happens. The particles are moving at a greater speed. Here is a task for you. Determine why that happens.
Now let us add two invisible buttons (start and stop) in the confetti component. The references to them will reside in the parent. The parent will simulate the click of these two buttons as needed. On click of them startAnimation and stopAnimation will be triggered without re-rendering the component. So our confetti component will return two additional buttons with the canvas.
Here startRef and stopRef are props from the parent. App.js will look like the following.
This one overcomes the concerns of retaining the particles array and providing a smooth animation experience. But there are some problems in this approach too apart from the ugly way of using the ref.
When the parent re-renders (because of parent state/prop change) the child (confetti component) will also re-render. Then all the local properties with our Confetti component will reset (as it is a stateless functional component). And our component will give some undesired result. To simulate, let’s introduce a re-render button in the parent App.js.
I have introduced a dummyState and a rerender button. On clicking the button the will dummyState change, the parent App.js will re-render and so will the child confetti/index.js. Now go to your browser, refresh the page. Click start -> stop animation will work fine. Now again refresh and now click start -> rerender -> stop. The animation will not stop.
[Task-2] Also, when the component unmounts, though the callback runs cancelAnimationFrame with a valid animationId, the request is not cancelled. I was not able to figure out why😔. If you can, please mention in the comment.
Stage 5: (commit)
Let’s get rid of the refs now. We can extract the common functionalities and properties of the confetti to a single class.
In the constructor, we can modify any property like waveAngle, colors, timeout of the this.options property for the class from the options prop. I have added a new method ummountCanvas (a small spelling mistake here 🙃). The startAnimation method is also modified to accommodate timeout. Please see the committed files for details.
Now we need to create an instance of the class inside our react component and export this out of confetti.
Look closer. I have wrapped the Confetti component inside an IIFE. let canvas = null is outside the functional component and on component mount we create a new instance of canvas with HTMLcanvas reference and options passed from prop. On unmount canvas.ummountCanvas (class instance) is called. This approach keeps our canvas instance immune to component re-render.
Our App.js will now have a state streamAnimation which will be passed as a prop to our confetti/index.js component, to convey it when to start and stop animation.
Now this approach seems to solve all our problems. The rerender does not affect our animation and on component unmount frame requests are cancelled. So yeah!!! We have achieved our desired result. Correct? But wait. Do you remember we had discussed four goals before we started. Take a pause, scroll up and see if the goals are fulfilled.
Were you able to figure it out? The fourth goal, we should be able to create multiple instances of the confetti in a single parent, will fail. Reason is we have declared let canvas = null in IIFE’s scope. That means this will run when JS parses the code for the first time and all the instances of the returned functional component from the IIFE are using the same canvas variable to store the BridalConfetti instance. (canvas = new BridalConfetti(canvasRef.current, options)). Modify your App.js as mentioned below.
Solution: We have to move canvas to state. That way each instance will have their own BridalConfetti instance.
Stage 6: (commit)
Let’s move the canvas variable to component state.
Now multiple instances in the parent will work perfectly. The only problem is when the component will unmount the canvas.ummountCanvas() will break. This is due to the limitation of react effect hook to retain component state during unmount.
So to make this work we should replace the functional component with the class component. Because componentWillUnmount will have the retained state.
Now we have our fully functional confetti component. You can move it to a common place, import it and use it whenever needed.
Thank you!! 🙂