Consuming a REST API in React with Axios

Welcome to the last part of this series. Here we'll be creating the frontend for the Notes application. Familiarity with react is needed for this tutorial but you don't need to be an expert, basic knowledge is good enough for you to follow and understand. The first objective is to get the app up and running, styling will be done at the end.

If you come across this part first, you can check out parts 1 and 2. We already handled the backend setup and development in those tutorials.

We'll continue from where we stopped in part 2; so this would be easy to follow as well.

Let's get started!

Set-up react application directory

Navigate to the frontend application directory.

cd frontend

There happens to be a lot of files in the frontend directory that we won't make use of in the react application.

public folder

The important file here is the index.html file. You can delete all other files here. Don't forget to go inside the index.html file to delete the links to the manifest.json and logos. You can keep the react favicon or change it to a favicon of your choice. You can customize yours here.

src folder

Delete all the files in the src folder except the index.js file. Then create two new folders components and css in the src folder. Inside the components folder, create the following files. App.jsx Notes.jsx and List.jsx and inside the css folder create the index.css file.
The frontend directory should currently look like 👇
Frontend directory

index.js

Remove the webvitals import and the webvitals function at the end of the file as we won't be making use of them. Since we have changed the location of the App.jsx component we need to change the path of the App import to this

import App from './components/App'

and that of the css import to

import './css/index.css'

The index.js file should look like 👇

import React from 'react'
import ReactDOM from 'react-dom'
import './css/index.css'
import App from './components/App'

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

To make requests to the API endpoints on the Django backend server, we will need a JavaScript library called axios.

Axios is an HTTP client library that allows you to make requests to a given API endpoint, you can find out more here.

First, we'll install it using npm:

npm install axios

package.json

Next, open the package.json file and add the proxy below the "private": true, line so it ends up like 👇.

"name": "frontend",
  "version": "0.1.0",
  "private": true,
  "proxy": "http://localhost:8000",

This will make it possible for you to use relative paths when you are making the API requests. Instead of making use of http://localhost:8000/notes/ you can simply make use of /notes/. Seems like a great idea right?. You'll see it in action shortly. Now let's work on the component files.

List.jsx

Let's start with the List component. We won't be doing much here yet, we just need to simply declare and export the function.

function List(){

    return (
        <div className="note">

        </div>
    )
  }

export default List

Notes.jsx

First we import the required hooks; useState and useEffect. You can read more about react hooks here. We also need to import axios and the List component we created above.

import {useState, useEffect} from "react"
import axios from "axios"
import  List from "./List"

useState

Next, we create the Note function in which we will make use of the useState hook. In the first line, we declare the state variable as notes with an initial state of null.

The second line is to handle the state of the form data. Here we declare the state variable as formNote with empty strings as its initial state.

function Note() {
    const [notes , setNewNotes] = useState(null)
    const [formNote, setFormNote] = useState({
          title: "",
          content: ""
          })
    }

Please note that every other function created below should be inside the Note function above.

useEffect

We'll also use the useEffect hook, so that the getNotes function executes right after the render has been displayed on the screen.

useEffect(() => {
      getNotes()
        } ,[])

To prevent the function from running in an infinite loop, you can pass an empty array ([]) as a second argument. This tells React that the effect doesn’t depend on any values from props or state, so it never needs to be re-run.

GET API function

function getNotes() {
  axios({
      method: "GET",
      url:"/notes/",
    }).then((response)=>{
      const data = response.data
      setNewNotes(data)
    }).catch((error) => {
      if (error.response) {
        console.log(error.response);
        console.log(error.response.status);
        console.log(error.response.headers);
        }
    })}

Here we are declaring the request method type as GET and then passing the relative path /notes/ as the URL. If we had not added the proxy "http://localhost:8000" to the package.json file. We would need to declare the URL here as "http://localhost:8000/notes/". I believe the method we used makes the code cleaner.

When the GET request is made with axios, the data in the received response is assigned to the setNewNotes function, and this updates the state variable notes with a new state. Thus the value of the state variable changes from null to the data in the received response.

We also have the error handling function in case something goes wrong with the get request.

POST API function

function createNote(event) {
    axios({
      method: "POST",
      url:"/notes/",
      data:{
        title: formNote.title,
        content: formNote.content
       }
    })
    .then((response) => {
      getNotes()
    })

    setFormNote(({
      title: "",
      content: ""}))

    event.preventDefault()
}

Here we are declaring the request method type as POST and then passing the relative path /notes/ as the URL. We also have an additional field here data. This will contain the data which we'll send to the backend for processing and storage in the database. That is the data from the title and content inputs in the form.

When the POST request is made with Axios, we don't process the response (remember that this was mentioned in part 2 when we were setting up the POST API function); we just use the response function to recall the getNotes function so that the previous notes can be displayed together with the newly added note.

After this, we reset the form inputs to empty strings using the setFormNote function. Then we also have to ensure that the form submission does not make the page reload so we add the event.preventDefault function which prevents the default action of the form submission.

DELETE API function

function DeleteNote(id) {
    axios({
      method: "DELETE",
      url:`/notes/${id}/`,
    })
    .then((response) => {
      getNotes()
    });
}

We create the function with an id parameter so that we can pass the id of the particular note which we want to delete as an argument later on.

When the DELETE request is made with Axios, we don't process the response as well; we just use the response function to call the getNotes function so that the notes get method can get executed once again and we'll now see the remaining notes retrieved from the database.

form input change

We need to ensure that the input is a controlled one, so we handle the changes with the code below.

function handleChange(event) { 
  const {value, name} = event.target
  setFormNote(prevNote => ({
      ...prevNote, [name]: value})
  )}

The function monitors every single change in the form inputs and updates/delete where necessary. Without this function, you won't see what you are typing in the form input fields and the values of your input elements won't change as well. We de-structure event.target to get the value and name then we use the spread syntax to retain the value of the previous input and finally we assign a new value to the particular input being worked on.

return

Now we return the React elements to be displayed as the output of the Note function.

return (
<div className=''>

      <form className="create-note">
          <input onChange={handleChange} text={formNote.title} name="title" placeholder="Title" value={formNote.title} />
          <textarea onChange={handleChange} name="content" placeholder="Take a note..." value={formNote.content} />
          <button onClick={createNote}>Create Post</button>
      </form>
          { notes && notes.map(note => <List
          key={note.id}
          id={note.id}
          title={note.title}
          content={note.content} 
          deletion ={DeleteNote}
          />
          )}

    </div>
  );

In the form, we add the input and text area elements. Then we add the onChange event handler which calls the handleChange function when we make any change to the input fields. Then in the next line where we render the List component, we need to first confirm that at least one single note was retrieved from the database so that we don't pass null data to the List component.

If notes were actually retrieved with the GET function; we pass the content of the data (id, title, content) and also the delete function to the List component.

Finally don't forget to export the Note component so it can be used in the App.jsx file.

export default Note;

The Notes.jsx file should currently look like 👇

import {useState, useEffect} from "react";
import axios from "axios";
import List from "./List"

function Note() {

    const [notes , setNewNotes] = useState(null)
    const [formNote, setFormNote] = useState({
      title: "",
      content: ""
    })

    useEffect(() => {
      getNotes()
        } ,[])

    function getNotes() {
      axios({
          method: "GET",
          url:"/notes/",
        }).then((response)=>{
          const data = response.data
          setNewNotes(data)
        }).catch((error) => {
          if (error.response) {
            console.log(error.response);
            console.log(error.response.status);
            console.log(error.response.headers);
            }
        })}

    function createNote(event) {
        axios({
          method: "POST",
          url:"/notes/",
          data:{
            title: formNote.title,
            content: formNote.content
           }
        })
        .then((response) => {
          getNotes()
        })

        setFormNote(({
          title: "",
          content: ""}))

        event.preventDefault()
    }

    function DeleteNote(id) {
        axios({
          method: "DELETE",
          url:`/notes/${id}/`,
        })
        .then((response) => {
          getNotes()
        })
    }

    function handleChange(event) { 
        const {value, name} = event.target
        setFormNote(prevNote => ({
            ...prevNote, [name]: value})
        )}


  return (

     <div className=''>

        <form className="create-note">
          <input onChange={handleChange} text={formNote.title} name="title" placeholder="Title" value={formNote.title} />
          <textarea onChange={handleChange} name="content" placeholder="Take a note..." value={formNote.content} />
          <button onClick={createNote}>Create Post</button>
        </form>

        { notes && notes.map(note => <List
        key={note.id}
        id={note.id}
        title={note.title}
        content={note.content} 
        deletion ={DeleteNote}
        />
        )}

    </div>

  );
}

export default Note;

List.jsx

Now we have to go back to the List.jsx file to finish creating the List component.

function List(props){
      function handleClick(){
    props.deletion(props.id)
  }
    return (
        <div className="note">
          <h1 >  Title: {props.title} </h1>
          <p > Content: {props.content}</p>
          <button onClick={handleClick}>Delete</button>
        </div>
    )
  }

export default List;

Here we access the data sent from the Note function using props; which gives us access to the title, content and id of the note. We pass the id to an onClick function which in turn calls the delete function in the Note function with id as the argument.

Note: If you pass the delete function into the onClick function directly, the delete function will run automatically and delete all your notes. A solution to this is to pass the delete function into a function called by the onClick function just like we did above.

App.jsx

Now let us import the Note function into the App.jsx file.

import Note from "./Notes"

function App() {

  return (
    <div className='App'>
      <Note />

    </div>
  );
}

export default App;

To test the current state of the application, run:

npm run build

then return to the project1 directory that contains the manage.py file

cd ..

Finally we run:

python manage.py runserver

Here is what the fully functional application looks like now 👇.
Test Application

Styling

The final part of this tutorial is to style the Notes application and make it end up looking like👇.
Styled Application

Return to the frontend directory

cd frontend

Material UI icon

You need to install material ui icon to get the + icon. Run:

npm install @material-ui/icons

Notes.jsx

Import AddIcon from the installed material ui icon package into the Notes component

import AddIcon from "@material-ui/icons/Add";

Next, we want to make the text input and add button hidden until the text area input is clicked, we'll use useState hooks once again to achieve this.

const [isExpanded, setExpanded]= useState(false)
const [rows, setRows]= useState(1)

The first line displays or hides the text input and add button based on the state(false or true). Here we declare the state variable as isExpanded with an initial state of false so the text input and add button are hidden when the page is loaded.
Initial state
The second line determines the height of the text area input. Here we declare the state variable as rows with an initial state of 1

function NoteShow(){
    setExpanded(true)
    setRows(3)
   }

Next, we create a new function Noteshow which gets called when the text area input is clicked.

Let's make the necessary changes to the form inputs as well;

<form className="create-note">
  {isExpanded && <input onChange={handleChange} text={formNote.title} name="title" placeholder="Title" value={formNote.title} />}
  <textarea onClick={NoteShow} onChange={handleChange} name="content" placeholder="Take a note..." rows={rows} value={formNote.content} />
  {isExpanded && <button onClick={createNote}>
                    <AddIcon />
                </button>}
</form>

The isExpanded condition is added to the text input and button as explained earlier. When the textarea input is clicked, the NoteShow function is called and two things happen.
i) the setExpanded function is called with the argument true which changes the state to true and then the hidden components are displayed
ii) the setRows function is called with the argument 3
which changes the rows attribute of the textarea input to 3 thus increasing the height of the textarea input.

Then we add the imported icon to the button.

Finally, we add setExpanded(false) to the end of the createNote function

function createNote(event) {
        axios({
          method: "POST",
          url:"/notes/",
          data:{
            title: formNote.title,
            content: formNote.content
           }
        })
        .then((response) => {
          getNotes()
        })

        setFormNote(({
          title: "",
          content: ""}))
        setExpanded(false)
        event.preventDefault()
    }

so that upon submission of the form, the text input and button both go back to their hidden state.

This is the final state of the Note.jsx component 👇.

import {useState, useEffect} from "react";
import axios from "axios";
import List from "./List"
import AddIcon from "@material-ui/icons/Add";

function Note() {
    const [isExpanded, setExpanded]= useState(false)
    const [rows, setRows]= useState(1)

    const [notes , setNewNotes] = useState(null)
    const [formNote, setFormNote] = useState({
      title: "",
      content: ""
    })

    useEffect(() => {
      getNotes()
        } ,[])

    function getNotes() {
      axios({
          method: "GET",
          url:"/notes/",
        }).then((response)=>{
          const data = response.data
          setNewNotes(data)
        }).catch((error) => {
          if (error.response) {
            console.log(error.response);
            console.log(error.response.status);
            console.log(error.response.headers);
            }
        })}

    function createNote(event) {
        axios({
          method: "POST",
          url:"/notes/",
          data:{
            title: formNote.title,
            content: formNote.content
           }
        })
        .then((response) => {
          getNotes()
        })

        setFormNote(({
          title: "",
          content: ""}))
        setExpanded(false)
        event.preventDefault()
    }

    function DeleteNote(id) {
        axios({
          method: "DELETE",
          url:`/notes/${id}/`,
        })
        .then((response) => {
          getNotes()
        })
    }

    function handleChange(event) { 
        const {value, name} = event.target
        setFormNote(prevNote => ({
            ...prevNote, [name]: value})
        )}

    function NoteShow(){
        setExpanded(true)
        setRows(3)
      }

  return (

     <div className=''>

        <form className="create-note">
          {isExpanded && <input onChange={handleChange} text={formNote.title} name="title" placeholder="Title" value={formNote.title} />}
          <textarea onClick={NoteShow} onChange={handleChange} name="content" placeholder="Take a note..." rows={rows} value={formNote.content} />
          {isExpanded && <button onClick={createNote}>
                            <AddIcon />
                        </button>}
        </form>

        { notes && notes.map(note => <List
        key={note.id}
        id={note.id}
        title={note.title}
        content={note.content} 
        deletion ={DeleteNote}
        />
        )}

    </div>

  );
}

export default Note;

Header.jsx

Create a new component Header.jsx in the components folder. This will hold our header elements.

function Header() {
  return (
    <header>
      <h1>Notes</h1>
    </header>
  );
}
export default Header;

Footer.jsx

Create a new component Footer.jsx in the components folder.This will hold our footer elements.

function Footer() {
  const year = new Date().getFullYear();
  return (
    <footer>
      <p>Copyright{year}</p>
    </footer>
  );
}
export default Footer;

Here we simply run the Date().getFullYear() method to get the year of the current date and pass it to the p element in our footer.

App.jsx

We need to import the Header and Footer components into the App.jsx file and then call them.

import Note from "./Notes"
import Header from "./Header"
import Footer from "./Footer"

function App() {

  return (
    <div className='App'>

      <Header />
      <Note />
      <Footer />

    </div>
  );
}
export default App;

CSS

Head over to the github repo for the css codes; the classNames have already been included while we were building the application.

We have completed the development of the Notes Application with CREATE,READ and DELETE functionalities. You can explore and have fun with your application now.

To test it run:

npm run build

then return to the project1 directory that contains the manage.py file

cd ..

Finally we run:

python manage.py runserver

You should see the new magic we just created.

Here is the link to the github repo for this project. Cheers!!!

If you have any questions, feel free to drop them as a comment or send me a message on LinkedIn or Twitter and I'll ensure I respond as quickly as I can. Ciao 👋
obito

11