For My Next Trick: React in Rocket!

I wanted to finally create a light API to more easily use my Ethel app and start actually organizing my bottle collection. At this point, I have been working on this project for more than a year! How time flies!

I thought about spinning up a separate React app and then having it talk to Ethel's API clientside only. I decided it might be more interesting to attempt to build a SPA (Single Page Application) and serve it statically using Rocket's File Server capabilities.

Early on, I found some promising leads, but no working examples of serving a built React app from Rocket. Eventually, I found a GitHub issue filed in the Rocket repository discussing exactly my quandary! After massaging some paths, setting some route ranks, I was able to serve my built React App from my Rocket server. My goal in doing this is the ability to deploy this all in a single container and not have to worry about CORs or XSS, or doing any weird subdomain things to ensure my client side could talk to my API.

Key configurations I changed in order to facilitate this working:

  • Updated the react build script (I used the standard create-react-app command, found in React's Getting Started) to build to a root level /build folder
  • Updated my main.rs to use rocket::fs::{FileServer, relative, NamedFile};
  • Added functions for a fallback_url and for files in order to setup the ability to go to NamedFiles in our static assets (and have a fallback so our app won't panic if we mess up our links or urls)
  • Added ranked routes promoting our fronted routes over a backend routes, in order to allow the front-end SPA route(s) to take precedence and load our React app

Check out my commit in Github to see the complete changes! If you want to see it running locally:

  • cd into frontend
  • run npm install
  • create a .env file with the following contents: BUILD_PATH=../build (this is what tells react to output your built project to the root directory)
  • run npm run build to output the built assets
  • don't forget to fire up your rocket app if you haven't already with cargo run
  • navigate to localhost:8000/index.html and you should se the standard React app screen!

I am not super happy with the development ergonomics on this: it mean's I have to do a "true build" to see my React changes, rather than the more "normal" experience of the local react dev server which picks up your changes as you go. My plan for dev is to build things out using the dev server (npm start) and then testing more thoroughly with the build once I have the frontend in a good state.

Okay, next up. I do not want to create a bunch of UI from scratch. Let's pick a UI component library. There's the old standby, Material, but I am doing a lot of things I have done before, so I would rather use an interesting new library. I decided to try out Chakra UI this time around: looks interesting and very composable. Also... I like the name. 😂

The very first thing I want to do is replace the default React app page with a table showing our bottle list from the get bottles endpoint. I know I want to separate my calls to the API into a client, so I immediately created a client folder in my fronted to store my files for using the async fetch command.

I also am a fan of really small, pure, React components: they should be very thin and single concern. To hold my components I created a components folder, and can further subdivide it if needed (I could see having a components/bottle directory for example). Finally, to get started, let's declare some Chakra components we know we want and make sure the provider is available to our app:

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import {
  ChakraBaseProvider,
  extendBaseTheme,
  theme as chakraTheme,
} from '@chakra-ui/react';

const { Table } = chakraTheme.components

const theme = extendBaseTheme({
  components: {
    Table,
  },
});

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);

root.render(
  <React.StrictMode>
    <ChakraBaseProvider theme={theme}>
      <App />
    </ChakraBaseProvider>
  </React.StrictMode>
);

index.tsx after adding initial Chakra UI provider and specifying some components to load

I decided to specifically use the ChakraBaseProvider because I expect this app to be thin, and will not need many of the library's components: this reduces bloat and load time. Right now we need just a Table so we will add the Table component to our themed components.

Great! Now we are ready to start doing real things. My next step was to create my first client endpoint for getting bottles. I also wanted to be able to rapidly iterate on the dev server version of my React app without making api calls, so I decided to serve up some test data if the API call failed (this will be useful for testing later, as well, although I'm not in love with this structure and may change it later).

interface Bottle {
    id: number;
    name: string;
    category_id: number;
    sub_category_ids: number[];
    storage_id: number;
}

const getBottles = async () => {
    let bottles: Bottle[] = [];
    try {
        const response = await fetch(`http://localhost:8000/bottles`);
        bottles = await response.json();
    }
    catch(error) {
        console.error(error);
        bottles = testBottleData; // for local dev testing
    }

    return bottles;
};

const testBottleData: Bottle[] = [
    {
        id: 1,
        name: 'Bottle 1',
        category_id: 1,
        sub_category_ids: [1,2],
        storage_id: 1
    },
    {
        id: 2,
        name: 'Bottle 2',
        category_id: 1,
        sub_category_ids: [3,4],
        storage_id: 2
    },
    {
        id: 3,
        name: 'Bottle 3',
        category_id: 2,
        sub_category_ids: [1,5],
        storage_id: 1
    }
];
  
export { getBottles, testBottleData };
export type { Bottle };

client/bottles.ts with initial getBottles function and Bottle type

Okay, now we can get some data! First, let's start from our smallest component or unit of functionality: the table row of the bottle. Let's call it BottleRow:

import { Td, Tr } from "@chakra-ui/react";
import { Bottle } from "../client/bottles";

function BottleRow(props: { bottle: Bottle }) {
    const bottle = props.bottle;
    return (
      <Tr key={bottle.id}>
        <Td>{bottle.name}</Td>
        <Td>{bottle.category_id}</Td>
        <Td>{bottle.sub_category_ids.join(', ')}</Td>
        <Td>{bottle.storage_id}</Td>
      </Tr>
    );
}

export default BottleRow;

BottleRow.tsx component

Super thin, super simple, dead easy to test. Next we need our main table containing all of our bottle rows! BottleTable feels sensible.

import { TableContainer, Table, TableCaption, Thead, Tr, Th, Tbody, Tfoot } from "@chakra-ui/react";
import BottleRow from "./BottleRow";
import { Bottle } from "../client/bottles";

interface BottleTableProps {
    bottles: Bottle[];
}
function BottleTable(props: BottleTableProps) {
    const bottles = props.bottles;
    return (
        <div>
          <TableContainer>
            <Table variant='striped'>
              <TableCaption>Bottle List</TableCaption>
              <Thead>
                <Tr>
                  <Th>Name</Th>
                  <Th>Category</Th>
                  <Th>Sub Categories</Th>
                  <Th>Storage</Th>
                </Tr>
              </Thead>
              <Tbody>
                { bottles.map((bottle) => <BottleRow bottle={bottle} />)}
              </Tbody>
              <Tfoot>
                <Tr>
                  <Th>Name</Th>
                  <Th>Category</Th>
                  <Th>Sub Categories</Th>
                  <Th>Storage</Th>
                </Tr>
              </Tfoot>
            </Table>
          </TableContainer>
        </div>
      );
}

export default BottleTable;

BottleTable.tsx component

Great! The final step is to put it all together in our App.tsx, finally replacing the default React app rendering page. We will use a useEffect hook to get the data for the bottles array to feed our table:

import { useState, useEffect } from 'react';
import { Bottle, getBottles } from './client/bottles';
import './App.css';
import BottleTable from './components/BottleTable';

function App() {
  const [bottles, setBottles] = useState<Bottle[]>([]);
  
  useEffect(() => {
    getBottles().then(bottles => setBottles(bottles));
  }, []);

  return (
    <BottleTable bottles={bottles} />
  );

}

export default App;

App.tsx retrieving bottle data and using our BottleTable component

Our app file is now super thin due to extracting out our other functional components and composing our app from them. And, because we are defaulting to our test data in our client, when the localhost:3000 react dev server fails due to CORs, we will still be able to get an idea of the UI!

Screenshot of test data table render

Now let's build the app and see what the table looks like with real data!

Screenshot of real data table render

Great! Next time we will work on getting those nasty raw id's to show "pretty" data by looking up the other records we need. For now though, I am feeling good about everything we have accomplished: getting a react app to serve from our rust/rocket server via the file server feature, installing and playing with a new component library in Chakra UI, and getting our app to work both for "real" data as well as some local test data for rapid UI changes!

Perfect Summer Cider in P.O.G.

This (extremely hot and ridiculously dry) summer I have been on the hunt for refreshing ciders. Bellingham Cider Co. based in Bellingham, WA has delivered a banger for me: their P.O.G. cider, or Pomegranate Orange Guava! Per their website, the 6.7% ABV deliciousness:

[...] was inspired by a Hawaiian vacation and our longing to feel as if we’re still there. This thirst-quenching refresher will remind you of relaxing days on the beach.

Deliciously fruity without being overpoweringly sweet. Remains crisp and refreshing and surprisingly light. Probably lives on the dangerous list: way too easy to crush the quickly, especially in the ridiculous heat we have been experiencing lately!

Stay cool, my friends. Stay safe. Stay hydrated (and libated).