facebook

How to Build a CRUD application using React and Django

George Anderson
Frontend and backend development guru, lover of all things computer... and cats! Hopes to make your coder life a breeze by sharing some tips and tricks of the trade.
Posted on Jan 30th 2020
In this tutorial, we are going to build a CRUD application for storing books with React and Django. CRUD stands for Create, Read, Update and Delete, so basically this app would work like a library shelf where we can take a look at each book data being able to modify as we want or need.

Technologies Used

For this application, we will be using two very popular solutions for building user interfaces and APIs. The user interface of the application will be built using React – prior knowledge of React is not required for this tutorial.

The back-end for this application will be built using Django, which is a high-level Python web framework that encourages rapid development and clean pragmatic design. Building APIs in Django is made easy using the Django Rest Framework.
We’ll be building the application in three stages:
1. Creating the front-end in React
2. Creating the APIs in Django
3. Connecting the front-end with the APIs

Getting Started: Configuring the Development Environment

For this tutorial, we will be using the Eclipse IDE with the CodeMix plugin installed. However, feel free to follow along in any IDE that supports both React and Python development.

  • Download Eclipse IDE here.
  • CodeMix can be installed from the Eclipse marketplace or via genuitec.com.
  • If the React pack isn’t already installed, install it using the CodeMix Extension Browser(Help > CodeMix Extensions). It can also be installed directly from the Eclipse marketplace.

Also, we need to have Python installed in our system, so let’s be sure about having it correctly installed. It can be downloaded here.

Creating the front-end in React

We will create our React application using the CodeMix wizard.
To create the application, navigate to File > New > Project > CodeMix > React Project.
Enter react-book-app as the name of the application in the dialog box that appears, then click finish.
The folder structure for your new CodeMix-created react application will look like:

Additional Packages

As you can see from the folder structure, there is no node_modules folder.
Hence, we need to install the node packages. Using Terminal+, run:
npm install
We will be using bootstrap and fontawesome for some additional styling and for some icons.
Add the following to the <head> section in src/index.js:
<link rel="stylesheet" href="https://bootswatch.com/4/lux/bootstrap.min.css">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">

Building the Interface

React is a component-driven library. So we need to break our app into different components. Here is the interface we are trying to build:
The interface can be broken into different components:

Book Dashboard Component:  This is the parent component in the application. It contains the book listing component and the Toggleable Book Form Component.

Book Listing Component: This component is saddled with the responsibility of listing all books that have been created in the application. It contains a list of Editable Book Components.
  • Editable Book component: Renders a Book component and a Book Form component. If the edit icon is clicked, then the Editable Book component will render a Book Form component that allows books that have been created to be edited.
  • Book component: This component shows the details of the book.
  • Book Form component: This component shows a form that allows a book to be created or edited.
Toggleable Book Form Component: This component has the responsibility of creating new books. When the  +  button is clicked, the Book Form component will be rendered which enables us to create a new book.
The application will be able to handle all 4 CRUD (Create Read Update Delete) operations.

Creating Components

We will create the components hierarchically — from the parent component. The first component to be created is the Book Dashboard component.

Modify src/index.js to look like:
class BookDashboard extends React.Component {
    state = {
        books: [
            {
                id: 1,
                title: 'A simple book',
                author: 'Jude Ben',
                description: `Lorem ipsum dolor sit amet, consectetur
                    adipiscing elit, sed do eiusmod tempor incididunt
                    ut labore et dolore magna aliqua. Ut enim ad minim
                    veniam, quis nostrud`
            },
            {
                id: 2,
                title: 'A book of secrets',
                author: 'James John',
                description: `Sed ut perspiciatis unde omnis iste natus
                    error sit voluptatem accusantium doloremque laudantium,
                    totam rem aperiam, eaque ipsa quae ab illo inventore
                    veritatis et quasi architecto beatae vitae dicta sunt
                    explicabo.`
            }
      ]
  }

  createNewBook = (book) => {
      book.id = Math.floor(Math.random() * 1000);
      this.setState({books: this.state.books.concat([book])});
  }

  updateBook = (newBook) => {
      const newBooks = this.state.books.map(book => {
          if(book.id === newBook.id) {
                return Object.assign({}, newBook)
          } else {
                return book;
          }
      });

      this.setState({books: newBooks});
  }

  deleteBook = (bookId) => {
      this.setState({books: this.state.books.filter(book => book.id !== bookId)})
  }
  render() {
      return (
          <main className="d-flex justify-content-center my-4">
              <div  className="col-5">
                  <BookList
                      books={this.state.books}
                      onDeleteClick={this.deleteBook}
                      onUpdateClick={this.updateBook}
                  />
                  <ToggleableBookForm
                      onBookCreate={this.createNewBook}
                  />
              </div>
          </main>
      )
  }
}
As seen above, the BookDashboard component stores the books in state. The books are then passed as props to the BookList component along with some functions to handle book deletion and updates.

BookDashboard also renders ToggleableBookForm and passes a function as prop which will handle book creation. State in React should be immutable, hence we go our handler functions: createNewBook, updateBook, deleteBook updates state without mutating the initial contents of a book.

Next, we need to create the BookList component for that, add the following code to index.js.
class BookList extends React.Component {
  render() {
    const books = this.props.books.map(book => (
      <EditableBook
        key={book.id}
        id={book.id}
        title={book.title}
        author={book.author}
        description={book.description}
        onDeleteClick={this.props.onDeleteClick}
        onUpdateClick={this.props.onUpdateClick}
      ></EditableBook>
    ));
    return (
      <div>
        {books}
      </div>
    );
  }
}

class EditableBook extends React.Component {
  state = {
    inEditMode: false
  };
  enterEditMode = () => {
    this.setState({inEditMode: true});
  }
  leaveEditMode = () => {
    this.setState({inEditMode: false});
  }
  handleDelete = () => {
    this.props.onDeleteClick(this.props.id);
  }
  handleUpdate = (book) => {
    this.leaveEditMode()
    book.id = this.props.id;
    this.props.onUpdateClick(book);
  }
  render() {
    const component = () => {
      if(this.state.inEditMode) {
        return (
          <BookForm 
            id={this.props.id}
            title={this.props.title}
            author={this.props.author}
            description={this.props.description}
            onCancelClick={this.leaveEditMode}
            onFormSubmit={this.handleUpdate}
          />
        );
      } 
      return (
        <Book 
          title={this.props.title}
          author={this.props.author}
          description={this.props.description}
          onEditClick={this.enterEditMode}
          onDeleteClick={this.handleDelete}
        />
      )
    }
    return (
      <div className="mb-3 p-4" style={{boxShadow: '0 0 10px #ccc'}} >
        {component()}
      </div>
    )
  }
}
The BookList component receives books as prop, maps over it and returns an array of EditableBook components which is then rendered. EditableBook is a component that renders a Book or a BookForm if the user clicks the edit book button on the Book component. It is a stateful component that stores the edit mode status in state and using that value, knows which component to render.

The book component receives the details about the book as props. It also receives some functions which handle the clicking of the edit button and the delete button.

Let’s create the Book and BookForm components.
class Book extends React.Component {
  render() {
    return (
      <div className="card" /* style="width: 18rem;" */>
        <div className="card-header d-flex justify-content-between">
          <span>
            <strong>Title: </strong>{this.props.title}
          </span>
          <div>
            <span onClick={this.props.onEditClick} className="mr-2"><i className="far fa-edit"></i></span>
            <span onClick={this.props.onDeleteClick}><i className="fas fa-trash"></i></span>
          </div>
        </div>
        <div className="card-body">
          {this.props.description}
        </div>
        <div className="card-footer">
          <strong>Author:</strong>  {this.props.author}
        </div>
      </div>
    );
  }
}

class BookForm extends React.Component {
  state = {
    title: this.props.title || '',
    author: this.props.author || '',
    description: this.props.description || ''
  }
  handleFormSubmit = (evt) => {
    evt.preventDefault();
    this.props.onFormSubmit({...this.state});
  }
  handleTitleUpdate = (evt) => {
    this.setState({title: evt.target.value});
  }
  handleAuthorUpdate = (evt) => {
    this.setState({author: evt.target.value});
  }
  handleDescriptionUpdate = (evt) => {
    this.setState({description: evt.target.value});
  }
  render() {
    const buttonText = this.props.id ? 'Update Book': 'Create Book';
    return (
      <form onSubmit={this.handleFormSubmit}>
        <div className="form-group">
          <label>
            Title
          </label>
          <input type="text" placeholder="Enter a title"
            value={this.state.title} onChange={this.handleTitleUpdate}
            className="form-control"
          />
        </div>
        
        <div className="form-group">
          <label>
            Author
          </label>
          <input type="text" placeholder="Author's name"
            value={this.state.author} onChange={this.handleAuthorUpdate}
            className="form-control"
          />
        </div>
        
        <div className="form-group">
          <label>
            Description
          </label>
          <textarea className="form-control" placeholder="Book Description"
            rows="5" value={this.state.description}
            onChange={this.handleDescriptionUpdate}
          >
            {this.state.description}
          </textarea>
        </div>
        <div className="form-group d-flex justify-content-between">
          <button type="submit" className="btn btn-md btn-primary">
            {buttonText}
          </button>
          <button type="button" className="btn btn-md btn-secondary" onClick={this.props.onCancelClick}>
            Cancel
          </button>
        </div>
      </form>
    )
  }
}
The Book component just renders the markup for displaying books. 

All the data it needs and handlers for edit and delete actions are gotten from the parent component — EditableBook.

The
BookForm component, on the other hand, renders a form that contains the book details from props for an update operation.

For a create operation, which will be handled by ToggleableBookForm, no props will be sent the form is made reactive by calling setState when the input field is modified. We do this by creating different handlers for each field and listen for the change event.

The function to handle submit events on this form will be sent as a prop from the parent component — EditableBook or ToggleableBookForm.
The last component to be built is the ToggleableBookForm, this component will be used for creating books.

// index.js
class ToggleableBookForm extends React.Component {
  state = {
    inCreateMode: false
  }
  handleCreateClick = () => {
    this.setState({inCreateMode: true});
  }
  leaveCreateMode = () => {
    this.setState({inCreateMode: false});
  }
  handleCancleClick = () => {
    this.leaveCreateMode();
  }
  handleFormSubmit = (book) => {
    this.leaveCreateMode();
    this.props.onBookCreate(book);
  }
  render() {
    if (this.state.inCreateMode) {
      return (
        <div className="mb-3 p-4" style={{boxShadow: '0 0 10px #ccc'}} >
          <BookForm 
            onFormSubmit={this.handleFormSubmit}
            onCancelClick={this.handleCancleClick}></BookForm>
        </div>
        
      )
    }
    return (
      <button onClick={this.handleCreateClick} className="btn btn-secondary">
        <i className="fas fa-plus"></i>
      </button>
    );
  }
}
As described earlier, ToggleableBookForm  renders a button which the user clicks to create a book. When the button gets clicked, the BookForm component is rendered.

Finally, we render the BookDashboard component using ReactDOM.
// index.js
ReactDOM.render(<BookDashboard />, document.getElementById('root'));
Reloading the browser, the book app should be functioning as expected.
Our app works!!!

However, the data is not persisted when the page reloads, this is because the data is stored in data structures in our application — state. To persist the data, we need some kind of data storage solution that our React app can communicate with. To do this, we will build a simple REST API with Python using the Django framework.

Building The Books API with Django

We’ll create the books API using Django, let’s create a new dummy project:
If you don’t have pipenv installed already, you will need to install it by running the following in the terminal:
pip install pipenv
Using the integrated terminal, activate the pipenv shell to create the Django project:
pipenv shell
When done, run the following commands to install Django and Django REST Framework:
pipenv install django
pipenv install django-rest-framework
Next, we create a Django project and create a Django app.
django-admin startproject booksApi
cd booksApi/
django-admin startapp books
This will create new folders in the books-api folder. Update INSTALLLED_APPS in settings.py to include books and django-rest-framework.
# books-api/booksApi/booksApi/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'books',
]
Our database model will just contain the Book Model. Add the following to models.py
# books-api/booksApi/books/model.py.

from django.db import models

# Create your models here.

class Book(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField()
    author = models.CharField(max_length=200)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return '%s by %s' % (self.title, self.author)
Our Book model contains 4 fields: title, description, author and created_at.
  • The title field stores the title of the book.
  • The description stores the description of a book.
  • The author field stores the author of the book.
These fields will be provided from our frontend — the react app we built earlier.
  • The created_at field will be auto inserted based on the time the book is created.
Run the following command to create the necessary migrations and tables.
In Terminal+,  ensure you are at books-api/booksApi/ then run:
python manage.py makemigrations
python manage.py migrate
Next, we will create a serializer. Serializers are used to convert our data to JSON which will be returned when we visit the endpoints.
# books-api/booksApi/books/serializers.py.
from rest_framework import serializers
from .models import Book


class BookSerializer(serializers.ModelSerializer):
  class Meta:
    model = Book
    fields = ('id', 'title', 'author', 'description', 'created_at')
Next, we update views.py to add our API views.
# books-api/booksApi/books/views.py.

from .models import Book
from .serializers import BookSerializer
from rest_framework import generics

class BookList(generics.ListCreateAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

class BookDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
Finally, we need to add our endpoints. Create a urls.py in books-api/booksApi/books
# books-api/booksApi/books/urls.py.
from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from books import views

urlpatterns = [
    path('books/', views.BookList.as_view()),
    path('books/<int:pk>/', views.BookDetail.as_view()),
]
And update books-api/booksApi/booksApi/urls.py to look like:

# books-api/booksApi/booksApi/urls.py.
"""booksApi URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/3.0/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('books.urls')),
]
Start the Django server by running the snippet below in Terminal+:
python manage.py runserver
The endpoint is can now be viewed at http://localhost:8000/api/books/. Before we consume the API in our react app, we need to solve one issue that will probably arise — CORS. Run the following to install a package to help with CORS:
pipenv install django-cors-headers
When that is done, add the following to the list of INSTALLED_APPS in settings.py as we did earlier. We will also need to add some new middleware.

# books-api/booksApi/booksApi/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'books',
    'corsheaders',

]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
]
And finally, add the line below to allow all origins to access the API:
# books-api/booksApi/booksApi/settings.py

CORS_ORIGIN_ALLOW_ALL = True
Run the server using the integrated terminal:
python manage.py runserver

Consuming the API with React

We will be using fetch to consume the books API. Add the following to index.js:
class BookDashboard extends React.Component {
    state = {
        books: []
    }

    componentDidMount() {
        fetch('http://localhost:8000/api/books/')
            .then(response => response.json())
            .then(data => {
                this.setState({books: data});
            });
    }
...
}
As seen above, we remove the initial content of the books array and we just initialize it with an empty array.

Using componentDidMount hook, we fetch the content of the book api and on line 10, update the state to use the data gotten.
Currently, there is no book in the database, so every call to http://localhost:8000/api/books/ will return an empty array.

Update the createNewBook function to use the API for book creation:

createNewBook = (book) => {
    fetch('http://localhost:8000/api/books/', {
        method: 'POST',
        headers: {
                'Content-Type': 'application/json',
        },
        body: JSON.stringify(book),
    })
    .then(response => response.json())
    .then(book => {
        this.setState({books: this.state.books.concat([book])});
    });
}
createNewBook sends a POST request to the books api with the book data gotten from the form. The API responds with the newly created book containing some additional data like id and created_at.

Next up, we need to modify books — update. To update a book, we need to send a PUT request.

updateBook = (newBook) => {
    fetch(`http://localhost:8000/api/books/${newBook.id}/`, {
        method: 'PUT',
        headers: {
                'Content-Type': 'application/json',
        },
        body: JSON.stringify(newBook),
    }).then(response => response.json())
    .then(newBook => {
        const newBooks = this.state.books.map(book => {
            if(book.id === newBook.id) {
                return Object.assign({}, newBook)
            } else {
                return book;
            }
        });
        this.setState({books: newBooks});
    });
As seen above, we are sending a put request to the url/<bookId>. This shows which book we are looking to update. If the update is successful, we update the state.

We are using the same state update mechanism as before. You might want to send another request to fetch all the books from the API but that will take more time.

The last method to modify is the delete action, just like this:
deleteBook = (bookId) => {
    fetch(`http://localhost:8000/api/books/${bookId}/`, {
        method: 'DELETE',
        headers: {
            'Content-Type': 'application/json',
        },
    })
    .then(() => {
        this.setState({books: this.state.books.filter(book => book.id !== bookId)})
    });
}
The delete action is probably the simplest. We send a DELETE request to the same URL as used for update, the delete action does not return any data so we can just update the state.

App running on Live Preview

Conclusion

With this functional CRUD application, we have just learned how to build a simple REST API using the Django framework, and a frontend in React which consumes the API. In case you want to take a closer look at the code, take a look at the Github repository for the React codebase, and for the Django API.
If you haven’t tried CodeMix yet, go and experiment with all the features it has to offer! Use the integrated LiveChat to receive full support from the CodeMix team right inside your IDE.

Have any questions or suggestions about this article or any of our services? Let us know your thoughts via Twitter or through the CodeMix forums. Happy Coding!

On a remote dev team? Try CodeTogether—it’s free!

• Live share IDEs & coding sessions
• See changes in real time
• Cross-IDE support for VS Code, IntelliJ & Eclipse
• Guests join from Browser or IDE
• End-to-end source encryption


www.codetogether.com