DEV Community

Cover image for Building out a Notes App with React and a Django Rest API Part 2
Kenneth Kimani
Kenneth Kimani

Posted on • Updated on

Building out a Notes App with React and a Django Rest API Part 2

Introduction

Welcome back to Part 2 of the React Django article series! In Part 1, we created a Django REST API for our notes app. Now, we're going to dive into the frontend development using React and Tailwind. We'll be building a dynamic UI that can interact with our Django REST API. Throughout this journey, we'll learn about component-driven development, using React hooks, and building responsive UIs using Tailwind CSS. By the end of this part, we'll have a fully functional notes app with a sleek and responsive UI. So, let's dive in!

Creating a React App:

Before we start building our notes app, we need to create a React app. This will provide us with a basic project structure and the necessary files to get started.

To create a new React app, In your IDE open your terminal and navigate to the directory where you want to create the app. Then, run the following command:

npx create-react-app <appname>

We can then move in the React app folder then start the server confirming it is working:

cd <appname>
npm run start

Reactstart

React Startup

Now that we have created our React app, let's install Tailwind CSS.

Installing Tailwind CSS:

Tailwind CSS is a utility-first CSS framework that makes it easy to style your React components. To install Tailwind, we'll use npm, the package manager that comes with Node.js.

In your terminal, run the following command to install Tailwind and its dependencies:

npm install -D tailwindcss
npx tailwindcss init

Install tailwindcss via npm, and create your tailwind.config.js file.

Add the paths to all of your template files in your tailwind.config.js file.

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./src/**/*.{html,js}"],  
  theme: {
    extend: {},
  },
  plugins: [],
}
Enter fullscreen mode Exit fullscreen mode

Add the @tailwind directives for each of Tailwind’s layers to your main CSS file.

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

This imports the base styles, components, and utility classes from Tailwind.

That's it! We have now created our React app and installed Tailwind CSS. In the next part of this article, we'll start building the UI for our notes app using React and Tailwind.

Apart from installing React and Tailwind, we also need to install Axios. Axios is a popular JavaScript library that is used to make HTTP requests from a web application. It is a promise-based library that provides an easy-to-use API for making asynchronous HTTP requests to REST endpoints and performing CRUD (Create, Read, Update, Delete) operations.

Axios is commonly used in React applications to fetch data from APIs or backend servers, and update or delete data on the server-side. In our case, we will use Axios to make HTTP requests to our Django Rest API and perform CRUD operations on our data.

To install Axios in your React project, you can use the following command in your terminal:

npm install axios

Creating Components

In our React app with Axios and Django REST API, we will be building multiple components that will work together to create a fully functional note-taking app. We will be building five components, including the NoteList, NoteForm, NoteDetail, Header, and Footer.

Now, you might be wondering why we are using components in our app. Components are the building blocks of a React app, allowing us to split our user interface into small, reusable pieces of code that can be easily managed and updated.

For example, we can create a component called NoteList, which will be responsible for displaying the list of notes created by the user. This component can be reused in different parts of our app, such as the home page, search page, and archive page. By creating a separate component for the NoteList, we can easily update or modify the way our notes are displayed without affecting the rest of the code.

Similarly, we can create a component for the NoteForm, which will allow users to create new notes, and a component for displaying the details of each note, called NoteDetail. Additionally, we can create components for the header and footer of our app, which will include the app logo, navigation links, and copyright information.

By breaking down our app into small, reusable components, we can easily modify or update a specific part of the app without affecting the rest of the code. This makes it easier to maintain and scale our app as it grows in complexity with Django REST API.

Great, lets start off with the NoteList component to display all notes, In the src folder create a components folder and inside the components folder lets create a NoteList.js component.

1.

import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';
import NoteForm from './NoteForm';

function NoteList() {
  // State variables for managing notes and note form visibility
  const [notes, setNotes] = useState([]);
  const [showNoteForm, setShowNoteForm] = useState(false);

  // Fetches notes from the API when the component mounts
  useEffect(() => {
    async function fetchNotes() {
      const response = await axios.get('http://localhost:8000/api/notes/');
      setNotes(response.data);
    }
    fetchNotes();
  }, []);

  // Deletes a note when the "Delete" button is clicked
  const handleDelete = async (id) => {
    try {
      await axios.delete(`http://localhost:8000/api/notes/${id}/`);
      // Removes the deleted note from the state array
      setNotes(notes.filter((note) => note.id !== id));
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <div className="container mx-auto px-4">
      {showNoteForm ? (
        // Renders the note form component when showNoteForm is true
        <NoteForm setNotes={setNotes} />
      ) : (
        <div>
          <div className="flex justify-between items-center my-8">
            <button
              // Sets showNoteForm to true when the "Create Note" button is clicked
              onClick={() => setShowNoteForm(true)}
              className="bg-blue-500 text-white font-medium py-2 px-4 rounded hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2"
            >
              Create Note
            </button>
          </div>
          {notes.length > 0 ? (
            // Renders a grid of note cards when there are notes to display
            <ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
              {notes.map((note) => (
                <li key={note.id} className="border border-gray-400 rounded-lg overflow-hidden shadow-md">
                  <Link to={`/notes/${note.id}/`} className="block">
                    <img src={note.cover_image} alt="" className="w-full h-40 object-cover" />
                    <div className="p-4">
                      <h2 className="text-lg font-medium text-gray-900">{note.title}</h2>
                      <p className="mt-2 text-gray-600">{note.body.slice(0, 100)}...</p>
                    </div>
                  </Link>
                  <div className="bg-gray-100 px-4 py-3">
                    <button
                      // Deletes the note when the "Delete" button is clicked
                      className="text-red-500 font-medium hover:text-red-600"
                      onClick={() => handleDelete(note.id)}
                    >
                      Delete
                    </button>
                  </div>
                </li>
              ))}
            </ul>
          ) : (
            // Renders a message when there are no notes to display
            <p>No notes found.</p>
          )}
        </div>
      )}
    </div>
  );
}

export default NoteList;
Enter fullscreen mode Exit fullscreen mode

This code defines a NoteList component in React that fetches a list of notes from an API, renders them as a list, and provides an option to delete each note.

The component uses useState and useEffect hooks to manage the state of the component. useState is used to manage the state of the notes array and to toggle the visibility of the NoteForm component, which is rendered when the "Create Note" button is clicked. useEffect is used to fetch the list of notes from the API when the component is mounted.

The handleDelete function is defined to delete a note from the list of notes and the API when the "Delete" button is clicked. This function uses the axios library to send a DELETE request to the API and then removes the note from the local notes array using the setNotes function.

The NoteList component renders either the NoteForm component or the list of notes depending on whether the showNoteForm state is true or false, respectively. If the list of notes is rendered, each note is rendered as a li element with a link to the note details page and a "Delete" button. If there are no notes in the notes array, a message is displayed indicating that no notes were found.

2.Next we have our Note Detail component:

import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import axios from "axios";
import NoteForm from "./NoteForm";
import moment from "moment"; // import moment here

function NoteDetail() {
  const { id } = useParams();
  const [note, setNote] = useState(null);
  const [editing, setEditing] = useState(false);

  // Fetch the note with the given id from the API when the component mounts
  useEffect(() => {
    axios.get(`http://localhost:8000/api/notes/${id}/`).then((response) => {
      setNote(response.data);
    });
  }, [id]);

  // Update the note in the API and local state when the form is submitted
  const handleUpdate = async (formData) => {
    try {
      const response = await axios.patch(
        `http://localhost:8000/api/notes/${id}/`,
        formData
      );
      setNote(response.data);
      setEditing(false);
    } catch (error) {
      console.error(error);
    }
  };

  // If the user is editing the note, show the NoteForm component
  if (editing) {
    return <NoteForm note={note} handleSubmit={handleUpdate} />;
  }

  // If the note hasn't been fetched yet, show a loading message
  if (!note) {
    return <div>Loading...</div>;
  }

  // Otherwise, render the note details
  return (
    <div className="max-w-lg mx-auto my-8">
      <img
        className="h-64 w-full object-cover mb-4"
        src={note.cover_image}
        alt={note.title}
      />
      <h1 className="text-2xl font-bold text-gray-800 mb-4">{note.title}</h1>
      <p className="text-gray-600 text-base mb-4">{note.body}</p>
      <p className="text-gray-500 text-sm">
        Last updated: {moment(note.updated).format("MMMM Do YYYY, h:mm:ss a")}
      </p>
      <button
        className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mt-4"
        onClick={() => setEditing(true)}
      >
        Edit Note
      </button>
    </div>
  );
}

export default NoteDetail;
Enter fullscreen mode Exit fullscreen mode

In summary, this component fetches a single note from the API based on the ID in the URL, and displays its details along with an "Edit" button. If the "Edit" button is clicked, the component switches to editing mode and shows the NoteForm component. When the form is submitted, the note is updated in the API and local state, and the component switches back to display mode. The moment library is used to format the "Last updated" timestamp.

3.Next we can now make our NoteForm to handle creating and editing notes:

import React, { useState, useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import axios from 'axios';

function NoteForm({ setNotes = () => {} }) {
  const navigate = useNavigate();
  const { id } = useParams();
  const [title, setTitle] = useState('');
  const [coverImage, setCoverImage] = useState(null);
  const [body, setBody] = useState('');

  useEffect(() => {
    if (id) {
      axios.get(`http://localhost:8000/api/notes/${id}/`).then((response) => {
        setTitle(response.data.title);
        setBody(response.data.body);
      });
    }
  }, [id]);

  const handleSubmit = async (e) => {
    e.preventDefault();
    const formData = new FormData();
    formData.append('title', title);
    formData.append('body', body);
    if (coverImage) {
      formData.append('cover_image', coverImage);
    }
    try {
      let response;
      if (id) {
        response = await axios.patch(`http://localhost:8000/api/notes/${id}/`, formData);
        setNotes((prevNotes) =>
          prevNotes.map((note) => (note.id === response.data.id ? response.data : note))
        );
        alert(`Note updated successfully!`, navigate(`/notes/${response.data.id}`));

      } else {
        response = await axios.post('http://localhost:8000/api/notes/', formData);
        setNotes((prevNotes) => [response.data, ...prevNotes]);
        alert('Note created successfully!', navigate('/'));
      }
      setTitle('');
      setBody('');
      setCoverImage(null);
    } catch (error) {
      console.error(error);
    }
  };


  return (
    <div className="container mx-auto px-4">
      <form onSubmit={handleSubmit}>
        <div className="mb-4">
          <label htmlFor="title" className="block font-medium text-gray-700">
            Title
          </label>
          <input
            type="text"
            name="title"
            id="title"
            value={title}
            onChange={(e) => setTitle(e.target.value)}
            required
            className="border border-gray-400 rounded w-full px-3 py-2 mt-1 text-gray-900"
          />
        </div>
        <div className="mb-4">
          <label htmlFor="cover_image" className="block font-medium text-gray-700">
            Cover Image
          </label>
          <input
            type="file"
            name="cover_image"
            id="cover_image"
            onChange={(e) => setCoverImage(e.target.files[0])}
            className="border border-gray-400 rounded w-full px-3 py-2 mt-1 text-gray-900"
          />
        </div>
        <div className="mb-4">
          <label htmlFor="body" className="block font-medium text-gray-700">
            Body
          </label>
          <textarea
            name="body"
            id="body"
            value={body}
            onChange={(e) => setBody(e.target.value)}
            required
            rows="5"
            className="border border-gray-400 rounded w-full px-3 py-2 mt-1 text-gray-900"
          ></textarea>
        </div>
        <div className="mt-6">
          <button
            type="submit"
            className="bg-blue-500 text-white font-medium py-2 px-4 rounded hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2"
          >
            {id ? 'Update Note' : 'Create Note'}
          </button>
        </div>
      </form>
    </div>
  );

}

export default NoteForm;

Enter fullscreen mode Exit fullscreen mode

This is a React functional component called NoteForm that creates a form for creating or updating notes. It uses hooks such as useState, useEffect and useParams to manage state and navigate between pages. It also uses the axios library to make HTTP requests to a backend server.

The component takes in a prop called setNotes which is a function that updates the list of notes. It initializes the navigate and id variables using the useNavigate and useParams hooks respectively. title, coverImage and body are states initialized using the useState hook.

The useEffect hook is used to fetch the details of an existing note using the id parameter passed in the URL. When the id changes, the useEffect hook is triggered and the note details are fetched using a GET request to the backend server.

The handleSubmit function is called when the form is submitted. It first creates a FormData object and appends the title, body, and coverImage fields to it. It then makes an HTTP request to the backend server to either update an existing note using a PATCH request or create a new note using a POST request. The setNotes function is called to update the list of notes with the updated or new note. Finally, the input fields are reset.

The return statement returns the JSX code for the form with input fields for the title, coverImage, and body. The button element is used to submit the form with either an 'Update Note' or 'Create Note' label depending on whether an id is present or not.

4.Next we create the Header.js component which will handle navigation and hold our light/dark mode functionality:

import { Link } from "react-router-dom";
import { useState } from "react";

function Header() {
  const [isDarkMode, setIsDarkMode] = useState(false);

  const handleDarkModeToggle = () => {
    setIsDarkMode(!isDarkMode);
    document.body.classList.toggle("bg-gray-800");
    document.body.classList.toggle("text-white");
  };

  return (
    <header className="flex justify-between items-center px-4 py-6">
      <Link to="/" className="text-2xl font-bold">
        Notes
      </Link>
      <div className="flex items-center">
        <button
          className="p-1 text-gray-700 dark:text-gray-300"
          onClick={handleDarkModeToggle}
        >
          {isDarkMode ? "☀️" : "🌙"}
        </button>
        <a
          href="https://github.com/your-github-link"
          target="_blank"
          rel="noreferrer"
          className="ml-4 text-gray-700 dark:text-gray-300"
        >
        </a>
      </div>
    </header>
  );
}

export default Header;
Enter fullscreen mode Exit fullscreen mode

This is a functional component named Header that is used to render the header section of the application. It imports two hooks, Link and useState, from the react-router-dom and react libraries, respectively.

Inside the Header component, there is a state variable isDarkMode that is initially set to false using the useState hook. The handleDarkModeToggle function is used to toggle the value of isDarkMode between true and false and to toggle the classes of the body element to change the background color and text color of the application.

The return statement of the component contains an HTML header element with two child elements. The first child element is a Link component that links to the home page of the application. The second child element is a div element that contains a button and a link to a GitHub repository. The button's text changes depending on whether the application is in dark mode or not.

we wil need to add a style.css in our src folder file to handle our dark-light mode feature

:root {
    --primary-color: #3b82f6;
    --secondary-color: #6b7280;
    --background-color: #f9fafb;
    --text-color: #111827;
  }

  .dark-mode {
    --primary-color: #93c5fd;
    --secondary-color: #cbd5e1;
    --background-color: #1f2937;
    --text-color: #f9fafb;
  }

Enter fullscreen mode Exit fullscreen mode

5.Next, we can do the Footer.js:

// This component represents the website's footer, which displays the creator's name and a link to their GitHub profile
function Footer() {
  return (
    <footer className="bg-blue-100 py-4 mt-12">
      <div className="container mx-auto text-center text-sm text-gray-500">
        Made with ❤️ by <a href="https://github.com/ki3ani">ki3ani</a>
      </div>
    </footer>
  );
}

export default Footer;

Enter fullscreen mode Exit fullscreen mode

6.And finally we have the App.js:

import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Header from "./components/Header";
import Footer from "./components/Footer";
import NoteList from "./components/NoteList";
import NoteDetail from "./components/NoteDetail";
import NoteForm from "./components/NoteForm";

function App() {
  return (
    <Router>
      <div className="flex flex-col h-screen">
        <Header />
        <main className="flex-1 overflow-y-auto">
          <Routes>
            <Route path="/" element={<NoteList />} />
            <Route path="/notes/:id" element={<NoteDetail />} />
            <Route path="/create" element={<NoteForm />} />
            <Route path="/edit/:id" element={<NoteForm />} />
          </Routes>
        </main>
        <Footer />
      </div>
    </Router>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

The App component is the entry point of the React application. It renders a Router component from React Router which defines the routes for the application. The Header and Footer components are also rendered inside the div tag with class name flex flex-col h-screen, and the main content is rendered inside the main tag with class name flex-1 overflow-y-auto. The Routes component from React Router maps the URLs to the appropriate components which are rendered using the element prop. The NoteList, NoteDetail, and NoteForm components are rendered based on the corresponding URLs: / for the NoteList, /notes/:id for the NoteDetail, and /create and /edit/:id for the NoteForm.

Configuring CORS

One common issue that can arise when making HTTP requests from a React application to a Django backend is the Cross-Origin Resource Sharing (CORS) error. This error occurs when the browser blocks a request because the domain or port of the requesting application does not match the domain or port of the server-side API.

To avoid CORS errors, you can configure your Django backend to accept requests from the domain and port of your React application. One way to do this is to use the django-cors-headers package, which adds CORS headers to your Django responses, We do this by:

*installing corsheaders:
python -m pip install django-cors-headers

*add it into our installed apps:

INSTALLED_APPS = [
    ...,
    "corsheaders",
    ...,
]
Enter fullscreen mode Exit fullscreen mode

*You will also need to add a middleware class to listen in on responses:

MIDDLEWARE = [
    ...,
    "corsheaders.middleware.CorsMiddleware",
    "django.middleware.common.CommonMiddleware",
    ...,
]
Enter fullscreen mode Exit fullscreen mode

*then we can configure the middleware's behaviour in your Django settings, for this tutorial we allow all origins:

CORS_ALLOW_ALL_ORIGINS = True
Enter fullscreen mode Exit fullscreen mode

Awesome with this now set up in the Django Api we can now use our React App to consume from our Rest Api.
Lets start off both servers:
python
python manage.py runserver

javascript
npm run start

Displaying our Front-end App

Great we can now see how our app looks like(from the client end):

1.Our Home-Page Displaying all Notes:

GET

CREATE

Displays all Notes a user can click to view the specific note, the user can also create a new note and delete existing notes

2.Our Note Detail:

GET NOTE

EDIT

Displays the specific note with more details whereas a user can be able to edit an existing note

3.Header

Dark

Our Header handles our funtionality for light/dark mode as well as navigation back to homepage which is our NoteList

In conclusion, we have successfully built the frontend of our web application using React and Tailwind CSS. We have learned how to create reusable components, manage state using hooks, and use Tailwind CSS classes to style our components. By leveraging the power of React and Tailwind, we have created a fast and responsive user interface for our app.

I hope that you have enjoyed reading this article and have learned something new. I welcome your feedback and suggestions on how we can improve the code or approach. If you want to check out the source code for this project, it is available on GitHub frontend. Stay tuned for part 3 of the series where we will be focusing on authentication and authorization of our web app. Thank you for reading!

Top comments (0)