How to use React for a CRUD application

In this article, I will create a ReactJS project connected to a Spring Boot backend to use the main CRUD operations. The CRUD operations are: Create, Read, Update and Delete items.

I won’t handle the backend part, as I’ve already written an article about that.

If you’re looking for a full detailed tutorial, watch this video.

All the code used in the article is available in my repository.

Project Creation

To create the frontend project with React, I use the following command:

npx create-nx-workspace@latest frontend

The previous command creates an empty standalone React project. For my CRUD application, I need nothing more.

Before starting to code, I will write my React project using Typescript. Which is close to Javascript.

Now, I have to think about which components I need to structure my project.

I will use a separate component for each CRUD operation. The App component will create them all. It will also contain the list of items for my CRUD application.

To be more specific, the items will be Gym Records. With the name of the exercise and the weight.

So, let’s look at the App component which renders each CRUD component from a list of records.

export function App() {
  const [records, setRecords] = React.useState<GymRecord[]>([]);

  ...

  return (
    <div className="main-component">
        <div className="vertical-container">
            {<div>
                <h2>Create</h2>
                <CreateContentBox onSubmit={handleCreateSubmit}/>
            </div>}
        </div>
        <div className="vertical-container">
            {<div>
                <h2>Read</h2>
                {records.map(record =>
                    <ReadContentBox key={`${record.id}-${record.exercise}-${record.weight}`} content={record}/>
                )}
            </div>}
        </div>
        <div className="vertical-container">
            {<div>
                <h2>Update</h2>
                {records.map(record =>
                    <UpdateContentBox key={`${record.id}-${record.exercise}-${record.weight}`} onSubmit={handleUpdateSubmit} content={record}/>
                )}
            </div>}
        </div>
        <div className="vertical-container">
            {<div>
                <h2>Delete</h2>
                {records.map(record =>
                    <DeleteContentBox key={`${record.id}-${record.exercise}-${record.weight}`} onSubmit={handleDeleteSubmit} content={record}/>
                )}
            </div>}
        </div>
     </div>
  );
}

export default App;

I see in the previous code snippet the declaration of the records list. I use useState of React to declare the list. This way, any change on the list will trigger hooks of React to refresh the page if necessary.

In the returned HTML, I iterate over the records list to create each CRUD component.

I don’t do it for the Create operation, as the Create operation doesn’t list the records. It’s only a form HTML to submit a new gym record.

Let’s see in detail each CRUD operation.

Read Operation

The Read operation must only display a single gym record. For this, the component accepts as an input parameter a GymRecord object.

export interface GymRecord {
    id: number;
    exercise: string;
    weight: number;
}

And now the code of the Read component.

import React from 'react';

import './ContentBox.css';

import {GymRecord} from '../entities/GymRecord';

interface ContentBoxProps {
    content: GymRecord;
}

const ReadContentBox: React.FC<ContentBoxProps> = ({ content }) => {
    const [record, setRecord] = React.useState<GymRecord>(content);

    return (
        <div className="content-box">
            <p>Exercise: {record.exercise}</p>
            <p>Weight: {record.weight} kg</p>
        </div>
    );
};

export default ReadContentBox;

I declare as an input field a single gym record. As the Read component will only render a single record. It’s the App component which iterates over the list of records to generate as many Read components as gym records in the list.

  React.useEffect(() => {
    fetch("http://localhost:8080/gym/records", {
        method: "GET"
    }).then(response => {
        if (response.status == 200) {
            return response.json();
        }
        return null;
    }).then(data => {
        if (data !== null) {
            setRecords(data);
        }
    });
  }, []);

And I must add in the App component a useEffect to fetch all the gym records from my backend at the startup.

Create Operation

Let’s continue with the Create operation.

This time, I need an HTML form, as the Create operation will send information to the backend. It will send the information for a new gym record.

The input parameter of my component is now a reference to a method to post the gym record to the backend.

import React from 'react';
import './ContentBox.css';

interface ContentBoxProps {
    onSubmit: (exercise: string, weight: number) => void;
}

const CreateContentBox: React.FC<ContentBoxProps> = ({ onSubmit }) => {
    const [exercise, setExercise] = React.useState("");
    const [weight, setWeight] = React.useState<number>();

    const handleSubmit = () => {
        onSubmit(exercise, weight || 0);
        setExercise('');
        setWeight(0);
    };

    return (
        <div className="content-box">
            <input
                type="text"
                value={exercise}
                onChange={(e) => setExercise(e.target.value)}
                placeholder="Exercise name"
            />
            <input
                type="number"
                value={weight}
                onChange={(e) => setWeight(parseInt(e.target.value))}
                placeholder="Weight in Kg"
            />
            <button onClick={handleSubmit}>Create</button>
        </div>
    );
};

export default CreateContentBox;

Why don’t I create this method in the Create component?

This way, I delegate the backend communication to the App component. The App component is the one which has the list of gym records. Every time I create a new gym record, I add it directly to the list of records available in the App component.

Here is the submit method for the App component:

  const handleCreateSubmit = (exercise: string, weight: number) => {
    fetch("http://localhost:8080/gym/records", {
        method: "POST",
        headers: {"content-type": "application/json"},
        body: JSON.stringify({exercise: exercise, weight: weight})
    }).then(response => {
        if (response.status == 201) {
            return response.json()
        }
        return null;
    }).then(data => {
        if (data !== null) {
            setRecords([...records, data]);
        }
    });
  };

Delete Operation

For the Delete operation, I need to display the gym record to delete, and I need a method with the delete action.

This means that my Delete component needs two input parameters: the gym record to display, and the reference of a method to delete the current gym record.

import React from 'react';

import './ContentBox.css';

import {GymRecord} from '../entities/GymRecord';

interface ContentBoxProps {
    onSubmit: (id: number) => void;
    content: GymRecord;
}

const DeleteContentBox: React.FC<ContentBoxProps> = ({ onSubmit, content }) => {
    const [record, setRecord] = React.useState<GymRecord>(content);

    const handleSubmit = () => {
        onSubmit(record.id);
    };

    return (
        <div className="content-box">
            <p>Exercise: {record.exercise}</p>
            <p>Weight: {record.weight} kg</p>
            <button onClick={handleSubmit}>Delete</button>
        </div>
    );
};

export default DeleteContentBox;

The method to delete the gym record must call the backend with a DELETE HTTP verb. If the request is successful, also delete the same gym record from the records list of the App component.

Here is the method to delete the gym record in the App component:

  const handleDeleteSubmit = (id: number) => {
    fetch(`http://localhost:8080/gym/records/${id}`, {
          method: "DELETE",
          headers: {"content-type": "application/json"}
    }).then(response => {
          if (response.status == 200) {
              return response.json()
          }
          return null;
      }).then(data => {
          if (data !== null) {
              setRecords(records.filter(record => record.id !== data.id));
          }
      });
  };

Update Operation

Finally, the Update operation is the trickiest one.

As I can update field by field or replace the entire object.

When updating the fields one by one, I use the PATCH HTTP verb.

When replacing the entire entity, I use the PUT HTTP verb.

In this article, I replace the entire entity.

To do so, I use an HTML form where I display the values of a gym record. If the user wants to update one field or another, he can update the input fields and submit the form.

The form submit method will request the backend with a PUT HTTP verb and the values of all the fields.

import React from 'react';

import './ContentBox.css';

import {GymRecord} from '../entities/GymRecord';

interface ContentBoxProps {
    onSubmit: (record: GymRecord) => void;
    content: GymRecord;
}

const UpdateContentBox: React.FC<ContentBoxProps> = ({ onSubmit, content }) => {
    const [record, setRecord] = React.useState<GymRecord>(content);

    const handleSubmit = () => {
        onSubmit(record);
    };

    return (
        <div className="content-box">
            <input
                type="text"
                value={record.exercise}
                onChange={(e) => setRecord({...record, exercise: e.target.value})}
                placeholder="Exercise name"
            />
            <input
                type="number"
                value={record.weight}
                onChange={(e) => setRecord({...record, weight: parseInt(e.target.value)})}
                placeholder="Weight in Kg"
            />
            <button onClick={handleSubmit}>Update</button>
        </div>
    );
};

export default UpdateContentBox;

As before, the method to request the backend is also in the App component.

  const handleUpdateSubmit = (recordToUpdate: GymRecord) => {
    fetch(`http://localhost:8080/gym/records/${recordToUpdate.id}`, {
          method: "PUT",
          headers: {"content-type": "application/json"},
          body: JSON.stringify({exercise: recordToUpdate.exercise, weight: recordToUpdate.weight})
    }).then(response => {
          if (response.status == 200) {
              return response.json()
          }
          return null;
      }).then(data => {
          if (data !== null) {
            setRecords(records.map(record => (record.id === data.id ? {...record, exercise: data.exercise, weight: data.weight} : record)));
          }
      });
  };

Conclusion

There are some considerations when creating a CRUD application with React.

In the beginning, I used the key HTML tag in each CRUD component. The key HTML tag is necessary to allow React to distinguish each element in a list.

This way, when I modify the records list, React knows which item of the list was modified. And React will only re-render the impacted element, not the entire list.

Using all the fields (id, exercise and weight) in the key HTML tag is necessary, as the Update operation may update fields individually and maintain the same id.

My New ebook, How to Master Git With 20 Commands, is available now.

Leave a comment

A WordPress.com Website.