#21 Migrating to React

January 30, 2023
See the code for this post on the migrate-to-react branch.

I started out writing the frontend application in vanilla Javascript. This was sufficient to illustrate the basic concepts. But soon, our frontend app is going to grow in the frontend significantly, introducing more complexity. To keep our sanity, let's use one of the better tools for the job.

I have experience with React and I love it. It allows me to split the UI into logical components and write those components in a declarative way. This is done using the .jsx syntax, which is basically writing HTML-like code directly within Javascript.

"Declarative" means we are not performing direct updates to components. We declare how the components should look based on the state passed to them. Any time the state changes, React takes care of updating the components for us. It does so in an optimized and performant way.

Setting up a React app is simple. Let's use the create-react-app utility from the creators of React. In the root directory of our app, run:

npx create-react-app frontend

This creates the frontend folder with the minimal setup for the project. It also sets up the entire toolchain required to compile .jsx files into .js. It also comes with a development server with live reloading, making frontend development very convenient. Once the setup is done, run npm start and head to http://localhost:3000/. Any change saved in source files is reloaded live to the browser.

All our frontend source files will now live in /app/frontend/src. Here's how I rewrote the SVG map from pure .js to .jsx:

// See the repository for the full list of changes
// ...
const coordsToObstacles = [];
obstacles.forEach(([xStart, xEnd, yStart, yEnd, color]) => {
let x = xStart;
while (x <= xEnd) {
let y = yStart;
while (y <= yEnd) {
coordsToObstacles[`${x}:${y}`] = color || '#d77a61';
y += 1;
}
x += 1;
}
});
const SVG = () => {
const squareSize = gridSize / gridCount;
const Obstacle = ({ type, x, y, color }) => (
<rect
width={squareSize}
height={squareSize}
x={x}
y={y}
fill={color}
stroke={color}
/>
);
const obstacleElems = [];
for (let [key, color] of Object.entries(coordsToObstacles)) {
const [x, y] = key.split(':');
obstacleElems.push(
<Obstacle
key={`${x}:${y}`}
x={x * squareSize}
y={y * squareSize}
color={color}
/>
);
}
return (
<svg
width={gridSize}
height={gridSize}
>
{obstacleElems}
</svg>
)
};
// ...

Again, the key difference is that we no longer perform direct updates to individual components. After the initial load, React renders the entire tree of HTML elements. When we change the state, React will traverse the tree and update the necessary components.

You might have noticed I removed the part where I generate the graph. I realized we won't need it in the frontend. The simulation engine will have its own graph for generating routes and it will provide the route to the frontend. So for now, all we need are the coordinates of obstacles.

Let's now deploy the app. Before we do so, we need to build our frontend bundle:

npm build

In this case, I chose to create my builds locally and push them to the repository. By doing this, we avoid having to download the entire node_modules on the production server (required for the build step). This would significantly slow down our deployments. To add the build step, add the following to deploy.sh:

cd frontend
npm run build
cd ..
git add .
git commit -m "build"
git push
# ...

Let's also adjust our server code to serve the optimized bundles from the build directory:

http.Handle("/", http.FileServer(http.Dir("./frontend/build")))

Finally, remove the static folder from the app's root directory. Also remove the COPY step from the Dockerfile that references it. Then go ahead and deploy the app to production.

See the code for this post on the migrate-to-react branch.