Create a Sticky Notes App using React useReducer Hook

React has a vast collection of hooks which makes it easier for the developers to access the props values into various components. In this app, today we will try to understand the functionality of react's useReducer hook.
The useReducer hook simply creates a store to keep track of the application state, you can then create actions to be performed (like add/delete/update) and then call those actions using the dispatch method of useReducer hook.

We will try to understand these concepts more clearly through code. Let's begin with our Sticky Notes app which will allow user to create and delete sticky notes and on the backend, we will be using the useReducer hook to manage the state of the application.

First, we will start off by creating a new react app using the npx create-react-app my-react-app command. Then, as always we will do the necessary clean up and then come to the App.js file where we will start by declaring the initial state of the app.
So, for this application, the initial state should have the following fields:

const initialState = {
  lastNoteCreatedAt: null,
  totalNotes: 0,
  notes:[]
}

Here is the description of these fields:

  • lastNoteCreatedAt: This will display the time when the last note was created
  • totalNotes: Total number of notes to display on the header
  • notes: Actual notes array which will store all our notes

Don't forget to import the useReducer and useState hooks at the top of the App.js file as follows:

import React,{useState, useReducer} from 'react'

Next, lets create a form and a textarea where user will enter his/her notes.

<form className="main-form" onSubmit={addNote}>
<textarea placeholder="Add Note" 
value={noteText}
onChange={(e)=>setNoteText(e.target.value)}
></textarea>
<button>Add</button>
 </form>

The value attribute in the corresponds to the state we will need using the useState hook:

const [noteText, setNoteText] = useState('')

Now, let's create our notesReducer where we will define what actions will take place in our app.

const notesReducer = (prevState, action) => {
  switch(action.type){
    case 'ADD_NOTE':
      const newNote = {
        lastNoteCreatedAt: new Date().toTimeString().slice(0,8),
        totalNotes:prevState.notes.length +1,
        notes:[...prevState.notes, action.payload]
      }
      // {console.log(newNote)}
      return newNote;


    default:
    return prevState;
  }
}

This notesReducer contains an existing state (which is called prevState in our case) and an action attribute which corresponds to the actions this reducer can perform. Our reducer's first action is 'ADD_NOTE action which will create a new note with a timestring and an array of existing notes plus the newer entry and also a record of total notes by adding one to the existing length of notes array.

Now, in the app, we have to call this reducer in the following manner:

const [notesState, dispatch] = useReducer(notesReducer,initialState)

Our addNote() method called when the form is submitted, needs to do the following:

  • return without doing anything if input is blank
  • create a new note with the contents you want to have in a note like the id (we have used uuid() package here to generate a unique id every time a note is created), the note text and a rotate value (which is solely for the styling purpose, it will slightly rotate each note with a different value)
  • dispatch the newly created note to the reducer store telling which action type is required on this note
  • set the note input to null again
const addNote = (e) => {
e.preventDefault();

if(!noteText){
  return;
}

const newNote = {
  id: uuid(),
  text: noteText,
  rotate: Math.floor(Math.random()*20)
}

dispatch({ type:'ADD_NOTE', payload:newNote})
setNoteText('')
}

We will use the map method from javascript to display our notes:

{notesState.notes.map((note)=> (
  <div className="note"
  style={{transform:`rotate(${note.rotate}deg)`}}
  key={note.id}
  draggable="true"
  onDragEnd={dropNote}
  >
 <h2 className="text">{note.text}</h2> 
 <button className="delete-btn" onClick={()=>deleteNote(note)}>X</button>
 </div>

))}

We have added draggable="true" functionality in order to allow user to smoothly drag the notes to a new position, this will also require creating following two functions:

const dropNote = (e) => {
e.target.style.left = `${e.pageX - 50}px`;
e.target.style.top = `${e.pageY - 50}px`;
}

const dragOver = (e) => {
  e.stopPropagation();
  e.preventDefault();
}

Since, this drag and drop functionality is out of the context of this post so I will not talk about it in much detail here, you can visit the details here.

Now, let's write the DELETE_NOTE action which will do the following:

  • maintain the previous state intact i-e don't touch the existing array
  • reduce the total number of notes by one
  • filter the notes array and remove the one which has to be deleted
case 'DELETE_NOTE':
      const deleteNote = {
        ...prevState,
        totalNotes: prevState.notes.length -1,
        notes: prevState.notes.filter(note=>note.id !== action.payload.id)

      }
      return deleteNote

We will call the DELETE_NOTE action in a deleteNote function called on clicking the delete button present with each of the notes:

const deleteNote = (id) => {
  console.log('delete')
dispatch({ type:'DELETE_NOTE', payload: id})
}

This brings an end to the code of our application. You can find the styling and complete code of this app here.
That's all folks, hope this article helps you to understand the useReducer hook concepts in React.
Happy coding...

17