How to Create a Figma-like Infinite Canvas in React

How to Create a Figma-like Infinite Canvas in React

I’d recently written about how to build an infinite canvas in WebGL, but as I started building more of my ideas on an infinite canvas, I realised how simple HTML is for prototyping. So I thought I’d merge both and build an infinite canvas in React. This allows me to leverage all the benefits that the browser gives that would have to be rewritten for WebGL: Drag and Drop, Text, iFrames etc.
To those who have not gone through the other post, I would recommend that you go through at least the Foundation section although going through all of it would help you understand the code that I have used here. In the previous post I mentioned how the logic could be implemented with any framework, and I meant it. And I will show how that implementation could look when brought into React.
Note: I have made a Code Sandbox so you can view the whole code and play with it here

Immediate Mode Rendering

Before we get into the implementation, let me introduce you to a concept in Computer Graphics called Immediate Mode Rendering. This will be needed for the performance benefits it brings.
In immediate mode, the scene (complete object model of the rendering primitives) is retained in the memory space of the client, instead of the graphics library. This implies that in an immediate mode application the lists of graphical objects to be rendered are kept by the client and are not saved by the graphics library API.
What this means in React-land is that we are not depending on React to render the whole application. React usually keeps the whole tree and re-renders parts it when the state changes. But can you imagine how that would work for an infinite canvas? React keeping track of every single component in infinity and seeing and running hooks for each of them and placing them? It would be incredibly inefficient to say the least.
Instead, we will control and tell React what to render. We will keep a 60fps loop and tell React which components to render and where to render them. We will hide any components that are outside our canvas so React has to do just enough rendering to display elements that are inside. It is not even aware of the elements outside the canvas.
How do we do this in React? We write a custom hook that runs a loop on every RequestAnimationFrame
The way this works is that when the app starts, we start a RenderLoop that keeps calling requestAnimationFrame, and depending on the fps , triggers the draw function, which sets a frame string based on the current time. Whenever this value changes, state is changed and so our root component will re render every fps frames a second.

Implementation

Now that we’ve covered the way we will be using React in the previous section and the foundations of projection in the previous blog post, we can dive into the implementation. We can keep our expected output the same as we did the last time, which is to create 9 large blocks to test the following
  • Zooming in and out of the canvas and seeing more blocks
  • Moving the camera around and seeing different blocks
Expected output of our program. It is a grid of many colors with texts on each block
Expected output of our program. It is a grid of many colors with texts on each block
Let us start with how we place the most foundational elements. We need a component that can place any element with global coordinates on the canvas.
It takes the screen that is visible (maintained by our CanvasStore, covered later), and positions it locally with respect to our screen. So, given our screen + global coordinates, this component converts it to local coordinates and places them on the screen.
We are yet to implement the inBounds function. This checks whether the rectangle with its global coordinates is visible in the canvas. The overlap logic is explained in detail in this stack overflow answer.
Now that we know how to place elements on our canvas, we can build our InfiniteCanvas component and place 9 large blocks with text elements inside of them
Now we should incorporate this into our UI by building our CanvasRoot and adding it to our ReactDOM:
We initialize the CanvasStore and set up the RenderLoop and simply call our InfiniteCanvas here.
We also need to pass in the width and height of our canvas container to the CanvasStore which we do through a hook that we have imported. Now the only part remaining is the CanvasStore, which looks like this
We have covered the logic behind this CanvasStore in our previous post, so if you’re still confused I urge you to go through that post for more clarity. The only additional part is the idea of a shouldRender boolean.
We don’t want to render the whole tree at 60fps if nothing has changed. So we set shouldRender only when something new has occurred in the UI. This is an optimization that should make our code more performant
This is what our output looks once we put it all together.
An infinite canvas with scrolling and pinch zooming
An infinite canvas with scrolling and pinch zooming
I hope you learned and are playing around with more infinite canvas experiences in many tools and frameworks!
Omer