Working Through Rust Web Programming pt 2: Building a Front End

Matthew MacFarquhar
7 min readMar 24, 2024

--

Introduction

In this series, I will be working through this book on Rust Web Programming. The book is quite large and in depth, but in these articles I will be distilling the big picture pieces of the book into bite size tutorials and walk throughs.

In this section, we will not be doing any Rust code, as most of what we need for the backend has been set up in part 1. While it is definitely possible to write front end logic in Rust and then compile it to Web Assembly, I believe the best tool for the job of front end development is — for now at least — still javascript or typescript. We will be following the book and building a js based front end which we will then turn into a desktop app using electron.

The backend work will be tracked in this github project https://github.com/mattmacf98/web_app. For this tutorial, I have put the code into this front end project https://github.com/mattmacf98/front_end, at this commit.

Setting up the React App

The book goes over creating the app with plain js, html and css and adding functionality inside our Rust web app to construct and serve those different views. This is an interesting aside, but not how we should build production web apps, so I am just going to skip to the point where we throw all that out and use React.

The App Page

We are making a single page App, all of our content will go inside of App.js

import {Component} from "react";
import "./App.css"

class App extends Component {
state = {
"pending_items": [],
"done_items": [],
"pending_items_count": 0,
"done_items_count": 0
};

render() {
return (
<div className={"App"}>
<div className={"mainContainer"}>
<div className="header">
<p>complete tasks: {this.state.done_items_count}</p>
<p>pending tasks: {this.state.pending_items_count}</p>
</div>

<h1>Pending Items</h1>
{this.state.pending_items}
<h1>Done Items</h1>
{this.state.done_items}
</div>
</div>
)
}
}

export default App;

We start out with a simple component with some state for our to do items and a section of html to render the todo items out to our viewer. One problem, our component has no way of getting the todo items from our backend server, let’s fix that.

Making API Calls

Making API calls is an integral part of any front end app. Data lives on the backend, and to get that data we are going to need to ask for it.

NOTE: in the previous section, we enabled very permissive CORS on our backend web server to allow our front end app to request data from it. In a production app, we should configure CORS to only work for our specific front end endpoint and our desired methods and headers.

 getItems() {
axios.get("http://127.0.0.1:8000/v1/item/get", {headers: {"token": "some_token"}})
.then(response => {
let pending_items = response.data["pending_items"];
let done_items = response.data["done_items"];

this.setState({
"pending_items": this.processItemValues(pending_items),
"done_items": this.processItemValues(done_items),
"pending_items_count": response.data["pending_item_count"],
"done_items_count": response.data["done_item_count"]
})
})
}

componentDidMount() {
this.getItems();
}

These two new functions will live inside our App.js. The componentDidMount is a callback that React understands so that when our page loads, we automatically call getItems() to update the state of the component.

In getItems, you can see we make a call to our backend web server and update the state with the values we retrieve. We will get to what processItemValues does next, but essentially it just returns some html for the items which we can use to display in our render call.

Components

React allows us to isolate pieces of the app into their own files which can then be referenced during rendering and inserted in place into the page. These are called components.

import {Component} from "react";
import axios from 'axios';
import ToDoItem from "./components/ToDoItem";
import CreateToDoItem from "./components/CreateToDoItem";
import "./App.css"

class App extends Component {
state = {
"pending_items": [],
"done_items": [],
"pending_items_count": 0,
"done_items_count": 0
};

getItems() {
axios.get("http://127.0.0.1:8000/v1/item/get", {headers: {"token": "some_token"}})
.then(response => {
let pending_items = response.data["pending_items"];
let done_items = response.data["done_items"];

this.setState({
"pending_items": this.processItemValues(pending_items),
"done_items": this.processItemValues(done_items),
"pending_items_count": response.data["pending_item_count"],
"done_items_count": response.data["done_item_count"]
})
})
}

componentDidMount() {
this.getItems();
}

handleReturnedState = (response) => {
let pending_items = response.data["pending_items"];
let done_items = response.data["done_items"];

this.setState({
"pending_items": this.processItemValues(pending_items),
"done_items": this.processItemValues(done_items),
"pending_items_count": response.data["pending_item_count"],
"done_items_count": response.data["done_item_count"]
})
}

processItemValues(items) {
const itemList = [];
items.forEach((item, _) => {
itemList.push(
<ToDoItem key={item.title + item.status}
title={item.title}
status={item.status}
passBackResponse={this.handleReturnedState}
/>
)
});
return itemList;
}

render() {
return (
<div className={"App"}>
<div className={"mainContainer"}>
<div className="header">
<p>complete tasks: {this.state.done_items_count}</p>
<p>pending tasks: {this.state.pending_items_count}</p>
</div>

<h1>Pending Items</h1>
{this.state.pending_items}
<h1>Done Items</h1>
{this.state.done_items}
<CreateToDoItem passBackResponse={this.handleReturnedState}/>
</div>
</div>
)
}
}

export default App;

To finish up our page we will use two components:

  • Our ToDoItem component will be created via our processItemValues function we mentioned earlier and contains the information needed to render one todo item.
  • Our CreateToDoItem component will handle the logic and presentation for a button to create new todo items.

Let’s look at the ToDoItem component.

import React, {Component} from 'react';
import axios from "axios";
import "../App.css";

class ToDoItem extends Component {
state = {
"title": this.props.title,
"status": this.props.status,
"button": this.processStatus(this.props.status)
};

processStatus(status) {
if (status === "PENDING") {
return "edit";
} else {
return "delete";
}
}

inverseStatus(status) {
if (status === "PENDING") {
return "DONE";
} else {
return "PENDING";
}
}

sendRequest() {
axios.post("http://127.0.0.1:8000/v1/item/" + this.state.button,
{
"title": this.state.title,
"status": this.inverseStatus(this.state.status)
},
{
headers: {"token": "some_token"}
})
.then(response => this.props.passBackResponse(response))
}

render() {
return (
<div className={"itemContainer"}>
<p>{this.state.title}</p>
<div className={"actionButton"} onClick={() => this.sendRequest()}>{this.state.button}</div>
</div>
);
}
}

export default ToDoItem;

We set our ToDo item’s state using the props passed in via our App component. What we return is a simple div with the title of the item and a button that when clicked will either edit the item to DONE if the to do item is in PENDING state or delete it if it is in DONE state.

This button action makes a call to our Rust backend server and then it will call send back the updated state returned by the server to our App component so that it can update its state accordingly.

In a more sophisticated app, we may extract these API calls into their own context so that the state can be shared across components without the need to add these passBackResponse functions.

CSS

Now our app is functional but it is quite ugly. Throughout our App and component files, we imported a global App.css where we store our styling information. This file will properly style our buttons, text and sections. It will also decide the layout of the page based on the screen size the app is being displayed on.

Summary

  • Use React (or another js framework) for front end development
  • In React, we make API calls to our backend server and trigger them using hooks like componentDidMount
  • We can separate our frontend components into different component files and combine them together when we render
  • We store the styling information inside of a css file

Building with Electron

Now that we have a React web app up and running, the work to turn it into a desktop app is actually quite simple.

Electron entry point

The electron-ifying of our app can be done by just adding a single js file! at public/electron.js.

const {app, BrowserWindow } = require("electron");
const path = require("path");
const isDev = require("electron-is-dev");

function createWindow() {
const mainWindow = new BrowserWindow({
width:800,
height:800,
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true,
contextIsolation: true
}
});
mainWindow.loadURL(isDev ? "http://localhost:3000" : `file://${path.join(__dirname, "../build/index.html")}`);
if (isDev) {
mainWindow.webContents.openDevTools();
}
}

app.whenReady().then(() => {
createWindow();
app.on("activate", function () {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
app.on("window-all-closed", function () {
if (process.platform !== "darwin") app.quit();
})
})

When our app is ready we create a window which is 800x800 and either point to the localhost url (if we are in dev mode) or the built files if we are using our production build version.

We also add some callbacks to handle closing and re-opening the desktop app. We will create a new window on open and close the app if there are no open windows left (except on macs).

Configuration

We need a couple of dependencies in our Package.json.

    "webpack": "4.28.3",
"cross-env": "^7.0.3",
"electron-is-dev": "^2.0.0"

and some devDependencies

"devDependencies": {
"concurrently": "^7.1.0",
"electron": "^18.0.1",
"electron-builder": "^22.14.13",
"wait-on": "^6.0.1"
}

then we need to update our build script to create the electron app

"electron-build": "electron-builder",
"build": "npm run react-build && npm run electron-build",
"start": "concurrently \"cross-env BROWSER=none npm run react-start\" \"wait-on http://localhost:3000 && electron .\""

NOTE: the concurrently command didn’t work well for me, what I ended up doing is just two commands in separate windows

cross-env BROWSER=none npm run react-start

and

electron .

lastly we will need to update the entry point

"main": "public/electron.js",

Summary

  • Making an Electron desktop app is as easy as creating a small wrapper js file to manage the desktop windows and setting up some package.json configuration

Conclusion

This was a small aside from the book’s deep dive into Rust, but now we have a nice GUI to play around with and test out our code instead of having to do everything via CURL and Postman. In the next section, we will be jumping back into our Rust code and swapping out our JSON file state management for a more robust postgreSQL setup.

--

--

Matthew MacFarquhar
Matthew MacFarquhar

Written by Matthew MacFarquhar

I am a software engineer working for Amazon living in SF/NYC.

No responses yet