Welcome to 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 dont need to be an expert, basic knowlegde 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.
Please Note: The code provided in the 3 parts of this tutorial is for educational purposes and is not intended for use in production. Thank you
If you come across this part first, you can check out part 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 wont 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 customise 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 👇
index.js
Remove the webvitals
import and the webvitals function at the end of the file as we wont 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 back-end 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
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 us of /notes/
. Seems like a great idea right?. You'll see it in action shortly. Now let's work on the component files.
List
Let's start with the List component. We wont 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
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 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 incase 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 wont see what you are typing in the form input fields and the values of your input elements wont 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
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. Solution to this is to pass the delete function into a function called by the onClick function just like we did above.
App
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 👇.
Styling
The final part of this tutorial is to style the application. This is how the Notes
application is going to look like👇.
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.
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 get's 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 parameter true
which changes the state to true and then the hidden components are displayed
ii) the setRows
function is called with the parameter 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 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
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
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
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 👋