DEV Community

Cover image for Fullstack app: Building the Front-End with Vite, React, Typescript, Hooks and Docker
Matheus Bernardes Spilari
Matheus Bernardes Spilari

Posted on

Fullstack app: Building the Front-End with Vite, React, Typescript, Hooks and Docker

This is the first part of a series where we will build a full-stack app, including the frontend using Vite with React and TypeScript, the backend using Spring Boot with a PostgreSQL database, and we will deploy the application on Render.


Intro

First, let's create a directory called fullstackapp. Inside it, create two directories: frontend and backend. Try to follow the structure below.

Project Structure

.
├── frontend/               # React frontend
│   ├── public/             
│   ├── src/                # React components and pages
│   ├── Dockerfile          # Dockerfile for React
│   └── package.json        # React dependencies
├── backend/                # Spring Boot backend
│   ├── src/                # Java code for APIs and services
│   ├── Dockerfile          # Dockerfile for Spring Boot
│   └── pom.xml             # Maven dependencies
├── docker-compose.yaml      # Docker Compose file to manage multi-container application
└── README.md               # Project documentation
Enter fullscreen mode Exit fullscreen mode

Then inside of frontend directory we are going to create our Vite app.

cd frontend
npm create vite@latest . -- --template react-ts
Enter fullscreen mode Exit fullscreen mode

This will initialize a simple app with React using TypeScript.

We are going to build a simple app, just a text area where you can write messages, and those messages will be displayed in a list on the screen. Some people call this a to-do app, and maybe they are right, but the objective of this article is to show how easy it is to build an app and deploy it to showcase or add to your portfolio.

Think of this as just a starting point; you can create a lot of things from here.


Code

Inside the src directory, we are going to create a directory called components and inside of that create three components:

1. Message List

MessageList.tsx

import { MessageListDto } from "../dto/MessageListDto";

const MessageList = ({ messages, eraseFunct }: MessageListDto) => {
  return (
    <section className="messageSection">
      <h1>Messages</h1>
      <ul>
        {messages.map((obj) => (
          <li key={obj.id}>
            {obj.message}{" "}
            <button
              onClick={() => {
                eraseFunct(obj.id);
              }}
            >
              Erase Message
            </button>
          </li>
        ))}
      </ul>
    </section>
  );
};

export { MessageList };

Enter fullscreen mode Exit fullscreen mode

2. Message Form

MessageForm.tsx

import { MessageFormDto } from "../dto/MessageFormDto";

const MessageForm = ({
  submitMessage,
  userMessage,
  writingMessage,
}: MessageFormDto) => {
  return (
    <section className="writeMessageSection">
      <h1>Write a message</h1>
      <form onSubmit={(event) => submitMessage(event)}>
        <textarea
          onChange={(event) => writingMessage(event.target.value)}
          value={userMessage}
        />
        <button type="submit" disabled={userMessage === ""}>
          Send message !
        </button>
      </form>
    </section>
  );
};

export { MessageForm };

Enter fullscreen mode Exit fullscreen mode

3. Message View Model

I recommend you to create a directory inside src called hooks and put this file inside of that.

useMessageViewModel.tsx

import { useEffect, useState } from "react";
import { MessageDto } from "../dto/MessageDto";

const useMessageViewModel = () => {
  const [messages, setMessages] = useState<MessageDto[]>([]);
  const [userMessage, setUserMessage] = useState("");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const getMessagesUrl = import.meta.env.VITE_GET_MESSAGES_URL;
  const postMessagesUrl = import.meta.env.VITE_POST_MESSAGES_URL;
  const deleteMessageUrl = import.meta.env.VITE_DELETE_MESSAGES_URL;

  const loadMessages = async () => {
    setLoading(true);
    setError(null);

    try {
      const response = await fetch(getMessagesUrl);
      const data = await response.json();
      setMessages(data);
    } catch (err) {
      setError(`Failed to load messages due to : ${err}`);
    } finally {
      setLoading(false);
    }
  };

  const submitMessage = async (event: React.FormEvent) => {
    event.preventDefault();
    setError(null);
    try {
      const response = await fetch(postMessagesUrl, {
        method: "POST",
        body: JSON.stringify({ message: userMessage }),
        headers: {
          "Content-Type": "application/json",
        },
      });

      if (!response.ok) {
        throw new Error("Failed to send message.");
      }

      setUserMessage("");
      await loadMessages();
    } catch (err) {
      setError("Error sending message.");
    }
  };

  const deleteMessage = async (id: String) => {
    await fetch(`${deleteMessageUrl}/${id}`, { method: "DELETE" });
    await loadMessages();
  };

  useEffect(() => {
    loadMessages();
  }, []);

  return {
    messages,
    userMessage,
    setUserMessage,
    submitMessage,
    deleteMessage,
    loading,
    error,
  };
};

export { useMessageViewModel };

Enter fullscreen mode Exit fullscreen mode

Explanation

const getMessagesUrl = import.meta.env.VITE_GET_MESSAGES_URL;
const postMessagesUrl = import.meta.env.VITE_POST_MESSAGES_URL;
const deleteMessageUrl = import.meta.env.VITE_DELETE_MESSAGES_URL;
Enter fullscreen mode Exit fullscreen mode

These are environment variables, where you are passing the endpoints/URL from your backend.

You need to follow the prefix syntax for Vite to recognize the environment variables. Example: VITE_YOUR_VARIABLE


Dto directory

Inside the src directory, we are going to create a directory called dto and inside of that create three components:

1. Message Dto

MessageDto.ts

type MessageDto = {
  message: string;
  id: string;
};

export type { MessageDto };

Enter fullscreen mode Exit fullscreen mode

2. Message Form Dto

MessageFormDto.ts

type MessageFormDto = {
  submitMessage: (event: React.FormEvent) => Promise<void>;
  userMessage: string;
  writingMessage: (text: string) => void;
};

export type { MessageFormDto };

Enter fullscreen mode Exit fullscreen mode

3. Message List Dto

MessageListDto.ts

import { MessageDto } from "./MessageDto";

type MessageListDto = {
  messages: MessageDto[];
  eraseFunct: (id: String) => void;
};

export type { MessageListDto };

Enter fullscreen mode Exit fullscreen mode

CSS

"We are not going to create complex CSS for this app, as the main objective of the article is to show how to deploy the app.

So, clear the App.css file and write this code:"

*{
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.main{
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;

  width: 100vw;
  height: 100vh;
}

.messageSection{
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.messageSection > ul{
  display:flex;
  flex-direction: column;
}

.writeMessageSection{
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.writeMessageSection > form {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-items: center;
}
.writeMessageSection > form > textarea{
  resize: none;
  width: 400px;
  height: 100px;

  margin: 20px 0px 20px 0px;
}

Enter fullscreen mode Exit fullscreen mode

App.tsx

Change the App.tsx file, created by vite, to this code below:

App.tsx

import "./App.css";
import { MessageForm } from "./components/MessageForm";
import { MessageList } from "./components/MessageList";
import { useMessageViewModel } from "./components/useMessageViewModel";

function App() {
  const {
    messages,
    setUserMessage,
    submitMessage,
    deleteMessage,
    userMessage,
    error,
    loading,
  } = useMessageViewModel();
  return (
    <>
      <main className="main">
        {loading ? (
          <p>Loading...</p>
        ) : (
          <MessageList messages={messages} eraseFunct={deleteMessage} />
        )}
        {error && <p>{error}</p>}
        <MessageForm
          writingMessage={setUserMessage}
          submitMessage={submitMessage}
          userMessage={userMessage}
        />
      </main>
    </>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Dockerfile

Create a Dockerfile on the root of the frontend directory, this file will help us on the deploy.

FROM node:20

WORKDIR /app

ARG VITE_GET_MESSAGES_URL
ARG VITE_POST_MESSAGES_URL
ARG VITE_DELETE_MESSAGES_URL

COPY ./package*.json ./

RUN npm install

COPY . .

RUN npm run build

EXPOSE "9090"

CMD [ "npm", "run", "preview", "--", "--port", "9090", "--host", "0.0.0.0" ]
Enter fullscreen mode Exit fullscreen mode

Explanation

ARG VITE_GET_MESSAGES_URL
ARG VITE_POST_MESSAGES_URL
ARG VITE_DELETE_MESSAGES_URL

Enter fullscreen mode Exit fullscreen mode

"We need to create the environment variables this way because Vite only injects them during build, not at runtime.

When deploying this application, we need to set these environment variables in the Render panel. However, if you are using another platform, this code will work as well."


CMD [ "npm", "run", "preview", "--", "--port", "9090", "--host", "0.0.0.0" ]
Enter fullscreen mode Exit fullscreen mode
  • npm run preview: runs a preview version of the app after the build.
  • port: the port you set to run the application.
  • host 0.0.0.0: make the application accessible outside the container.

Conclusion

Okay, with this, the frontend is ready for deployment. In the next article, we will build the backend using Spring Boot.

Thanks for reading !!


📍 Reference

👋 Talk to me

Top comments (0)