Fridge Door Part 3: Building a React Application

This article is part 3 in a multipart series that makes a message board using a Raspberry Pi and a Tidbyt.

Here are the parts in the series:

  1. Describe what we’re building (Part 1)
  2. Create a ReST service using Rocket (Part 2).
  3. Create a React application to both display and manage messages (this article).
  4. Integrate the ReST service and React application.
  5. Deploy the ReST service and React application to a Raspberry Pi.
  6. Create a Tidbyt application to display messages.

Prerequisites

Nodejs

Create the Application

In this part, we build a React application that makes service calls to the service from Part 2 to create and display messages. You can find more documentation about creating a React application here.

Open a terminal and type:

$ npx create-react-app fridge-door-web
$ cd fridge-door-web

You could run the boilerplate React app now, if you want, by typing npm start. Your project will compile, start, and launch a browser. You should see an image of an atom, slowly rotating, that looks like this:

The boilerplate React app

Create the Pages

We are going to create two pages, in the sense that Single Page Applications (SPAs) can have more than one page, and route between them using React Router. These pages are:

  • The message board, which displays messages
  • The form for creating new messages

Stop your application by pressing <ctrl-c>. Add React Router as a dependency by typing:

$ npm install react-router-dom --save

Then, add support for its BrowserRouter by modifying index.js:

import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

Most of that is the generated boilerplate. You just add an import for BrowserRouter and wrap the App tag in a BrowserRouter tag.

Create a directory called pages beneath the src directory to hold the code for the two pages. Inside the pages directory, create two files, MessageBoard.js and NewMessage.js, as shown below.

import React from 'react';
import { Link } from 'react-router-dom';

function MessageBoard() {
  return (
    <div>
      <h1>Message Board</h1>
      <Link to="/new">Add a message</Link>
    </div>
  );
}

export default MessageBoard;
import React from 'react';
import { Link } from 'react-router-dom';

function NewMessage() {
  return (
    <div>
      <h1>New Message</h1>
      <Link to="/">Back to message board</Link>
    </div>
  );
}

export default NewMessage;

These pages don’t offer much yet: a title and a link to the other page, but we add content later. Now, we wire them up in App.js:

import React from 'react';
import { Routes, Route } from 'react-router-dom';
import MessageBoard from './pages/MessageBoard';
import NewMessage from './pages/NewMessage';
import './App.css';

function App() {
  return (
    <div className="App">
      <Routes>
        <Route path="/" element={<MessageBoard />} />
        <Route path="/new" element={<NewMessage />} />
      </Routes>
    </div>
  );
}

export default App;

Run your application by typing npm start. You should see your Message Board page with an “Add a message” link. Click the link to see the New Message page.

Blank Message Board page
Blank New Message page

Display Messages

We create a React component to display a message. Beneath your src directory, create a components directory and add a file called Message.js. It receives a message object and displays its text property. The code looks like this:

import React from 'react';

function Message({ message }) {
  return (
    <div>
      {message.text}
    </div>
  );
}

export default Message;

To make sure this component works, open MessageBoard.js and import the Message component. Create a default message object so that, regardless of whether we retrieve message data from the service, we have something to show. Set up state to store the current message. Lastly, render the Message component. Your code now looks like this:

import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import Message from '../components/Message';

function MessageBoard() {
  const defaultMessage = { id: 0, text: "Hello from your fridge door" };
  const [message, setMessage] = useState(defaultMessage);

  return (
    <>
      <div><Message message={message} /></div>
      <div>
        <Link to="/new">Add a message</Link>
      </div>
    </>
  );
}

export default MessageBoard;

Your page looks like this:

The default message

Add CORS Support to the Service

Before we proceed with the React app, we must update the service to allow Cross-Origin Resource Sharing (CORS). Adding this support will allow us to call the service running on port 8000 from our React app running on port 3000. You can read more about CORS here.

Return to your fridge-door directory and open main.rs. We use the rocket_cors package to add the CORS headers for us, allowing CORS for GET, POST, and OPTIONS requests. Your updated main.rs should look like this:

#[macro_use]
extern crate rocket;

use std::str::FromStr;

use rocket_cors::{AllowedOrigins, CorsOptions};

mod db;

#[get("/cors")]
fn cors() -> &'static str {
    "Hello, CORS!"
}

#[launch]
fn rocket() -> _ {
    let cors = CorsOptions::default()
        .allowed_origins(AllowedOrigins::all())
        .allowed_methods(
            vec!["Get", "Post", "Options"]
                .into_iter()
                .map(|s| FromStr::from_str(s).unwrap())
                .collect(),
        )
        .to_cors()
        .expect("error while building CORS");

    rocket::build()
        .mount("/", routes![cors])
        .attach(cors)
        .attach(db::stage())
}

Yes, I meant to do this in Part 2, but I forgot. Sorry. Restart your service using cargo run.

Retrieve Messages

We display the default message when someone first visits the page, but we want to display messages sourced from our service. We set a timer to fire every 15 seconds. When the timer fires, we use fetch to request one message — the message after the one we’re currently displaying. We use the since_id parameter to pass our current message’s ID. If we get no message back, we display our default message and reset the ID to 0. Make sure your service is running, add this code, and watch your messages change.

import React, { useEffect, useState } from 'react';

// existing code

  useEffect(() => {
    const interval = setInterval(async () => {
      await updateMessage();
    }, 15000);
    return () => clearInterval(interval);
  });

  const updateMessage = async () => {
    const msg = await getMessage();
    setMessage(msg ? msg : defaultMessage);
  };

  const getMessage = async () => {
    let response;
    try {
      response = await fetch(
        `http://127.0.0.1:8000/messages?count=1&since_id=${message.id}`,
        {
          mode: "cors",
        }
      );
    } catch (error) {
      console.error('Error:', error);
      return null;
    }

    let messages;
    try {
      messages = await response.json();
    } catch (error) {
      console.error('Error:', error);
      return null;
    }

    return messages.length > 0 ? messages[0] : null;
  };

// existing code

Every 15 seconds, you should see a new message. If your messages aren’t changing, check the console in your browser for any error messages. Also check that you have unexpired messages in your service database, and add some if you don’t.

Improve the Display

Your message board functions, albeit unattractively. Let’s add some CSS to make this message board pop. When we retrieve a new message from the service, we increment a simple counter. This counter corresponds to CSS classes that set fonts and 508-compliant colors. Open App.css, import some fonts from Google Fonts, and create some classes.

@import url('https://fonts.googleapis.com/css2?family=Montserrat');
@import url('https://fonts.googleapis.com/css2?family=Caveat');
@import url('https://fonts.googleapis.com/css2?family=Secular+One');
@import url('https://fonts.googleapis.com/css2?family=Permanent+Marker');
@import url('https://fonts.googleapis.com/css2?family=Lexend');
@import url('https://fonts.googleapis.com/css2?family=Patrick+Hand');

.App {
  text-align: center;
}

.Message-Board {
  background-color: #282c34;
  min-height: 90vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(48px + 2vmin);
  color: white;
}

.Message-Board-Footer {
  min-height: 10vh;
}

.Message-Board-0 {
  background-color: #623b5a;
  color: #edafb8;
  font-family: Montserrat, sans-serif;
}

.Message-Board-0 a {
  color: #edafb8;
}

.Message-Board-1 {
  background-color: #e8ebf7;
  color: #00487c;
  font-family: Caveat, sans-serif;
}

.Message-Board-1 a {
  color: #00487c;
}

.Message-Board-2 {
  background-color: #93b48b;
  color: #111;
  font-family: 'Secular One', sans-serif;
}

.Message-Board-2 a {
  color: #111;
}

.Message-Board-3 {
  background-color: #423e28;
  color: #f1e9db;
  font-family: 'Permanent Marker', sans-serif;
}

.Message-Board-3 a {
  color: #f1e9db;
}

.Message-Board-4 {
  background-color: #555b6e;
  color: #f0ec57;
  font-family: 'Lexend', sans-serif;
}

.Message-Board-4 a {
  color: #f0ec57;
}

.Message-Board-5 {
  background-color: #66ced6;
  color: #283f3b;
  font-family: 'Patrick Hand', sans-serif;
}

.Message-Board-5 a {
  color: #283f3b;
}

We have six looks, numbered 0-5. Add a counter to MessageBoard.js that resets when it reaches the end, and add the classes to the div that hosts the Message component.

// existing code

  const maxCssIndex = 5;
  const [cssIndex, setCssIndex] = useState(0);

// existing code

  const updateMessage = async () => {
    const msg = await getMessage();
    setMessage(msg ? msg : defaultMessage);
    setCssIndex(cssIndex >= maxCssIndex ? 0 : cssIndex + 1);
  };

// existing code

  return (
    <>
      <div className={`Message-Board Message-Board-${cssIndex}`}>
        <Message message={message} /></div>
      <div className={`Message-Board-Footer Message-Board-${cssIndex}`}>
        <Link to="/new">Add a message</Link>
      </div>
    </>
  );

// existing code

Your React app should now show the Message Board in different colors and fonts.

CSS applied

Add the New Message Form

The final step to your React app is to build a simple form for adding a message. People can click the “Add a message” link to go to a screen with a text area and a Submit button to add a new message. First, add some CSS classes to App.css:

.New-Message {
  background-color: #282c34;
  min-height: 90vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(48px + 2vmin);
  color: white;
}

.New-Message-Footer {
  background-color: #282c34;
  min-height: 10vh;
}

.New-Message-Footer a {
  color: white;
}

Then, update NewMessage.js to show a form and POST the message to your service on submit:

import React, { useState } from 'react';
import { Link } from 'react-router-dom';

function NewMessage() {
  const [message, setMessage] = useState("");

  const handleSubmit = async (e) => {
    e.preventDefault();

    try {
      const response = await fetch("http://localhost:8000/messages", {
        method: "POST",
        mode: "cors",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          text: message,
        })
      });

      if (response.ok) {
        setMessage("");
      }
    } catch (error) {
      console.error('Error:', error);
    }
  };

  return (
    <>
      <div className="New-Message">
        <form onSubmit={handleSubmit}>
          <label>
            Message:
            <textarea value={message} onChange={(e) => setMessage(e.target.value)} rows="4" cols="60" />
          </label>
          <div>
            <input type="submit" value="Submit" />
          </div>
        </form>
      </div>
      <div className="New-Message-Footer">
        <Link to="/">Back to message board</Link>
      </div>
    </>
  );
}

export default NewMessage;

Click the link on your message board to view the form, which should look like this:

The New Message form

Enter a message and hit the Submit button. Click the link to return to your message board. Wait for your new message to appear.

A new message, added through the web form

Next Steps

We have a service that runs on port 8000, and a React app that runs on port 3000. The React app makes a ReST call to localhost:8000 to hit the service. This works well for development purposes, but not at all when we host both service and React app on a Raspberry Pi. In the next part in this series, we update our service to serve the React app, so that we can host both on a single endpoint, without any hard-coded host names or ports in the URLs.

2 Responses

  1. February 4, 2023

    […] Create a React application to both display and manage messages (Part 3). […]

  2. February 4, 2023

    […] Create a React application to both display and manage messages (Part 3). […]

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.