DEV Community

Himanshupal0001
Himanshupal0001

Posted on

Understanding CRUD operations with a note app in react

This post is use to demonstrate the use of CRUD operations in react using react.
CRUD stands for create/read/update/delete.
We also learn the following

  • Persist data using local storage
  • debouncing
  • props
  • hooks
  • Date formatter function

Following is the structure of the app

Image description

This app has 3 components Sidebar, Note Container, Notes.
App component will be the main component/parent component. All the signals pass through this component only. In another words all the children component talk to each other through app component.

First Lets create the bere bone of the project

SideBar Js

import React from 'react'
import './Sidebar.css'

export default function Sidebar() {

    return (
        <h1>Sidebar</h1>
    )
}

Enter fullscreen mode Exit fullscreen mode

SideBar css

.sidebar-container {
    height: 100%;
    width: 10%;
    display: flex;
    flex-direction: column;
    align-items: center;
}

svg {
    margin-bottom: 10px;
}

ul>li {
    height: 30px;
    width: 30px;
    border-radius: 50%;
    margin: 5px;
    list-style: none;
}
Enter fullscreen mode Exit fullscreen mode

Note Container js

import React from 'react'
import './NoteContainer.css'

export default function NoteContainer() {

    return (
        <h1>Note Container</h1>
    )
}
Enter fullscreen mode Exit fullscreen mode

Note Container css

.note-container {
    height: 100%;
}

.header {
    margin-bottom: 2rem;
}

.note-body {
    display: flex;
    flex-wrap: wrap;
    gap: 20px;
    overflow: auto;
    height: 90%;
}
Enter fullscreen mode Exit fullscreen mode

Notes Js

import React from 'react'
import './Note.css'
function Note() {

    return (
        <h1>Notes</h1>
    )
}
export default Note
Enter fullscreen mode Exit fullscreen mode

Notes css

.card-container {
    height: 300px;
    width: 250px;
    border-radius: 1rem;
}

.text-area {
    width: 100%;
    height: 80%;
    border: none;
    outline: none;
    resize: none;
    line-height: 1.875rem;
    padding: 1rem;
    background-color: transparent;
}

.footer {
    display: flex;
    align-items: center;
    justify-content: space-around;
}
Enter fullscreen mode Exit fullscreen mode

App js


import './App.css';
function App() {

  return (
    <div className="App">
      <Sidebar />
      <NoteContainer />
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

App css

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

.App {
  height: 100vh;
  display: flex;
}

.custom-scroll::-webkit-scrollbar {
  width: 8px;
}

.custom-scroll::-webkit-scrollbar-track {
  background-color: transparent;
}

.custom-scroll::-webkit-scrollbar-thumb {
  border-radius: 20px;
  background-color: lightgray;
  transition: 300ms;
}

.custom-scroll::-webkit-scrollbar-thumb:hover {
  background-color: gray;
}
Enter fullscreen mode Exit fullscreen mode

First lets create the sidebar

import React from 'react'
import './Sidebar.css'

export default function Sidebar() {

 const color = ['#fe9b72', '#fec971', "#00d4fe", '#b693fd', '#e4ee91']
    const [open, setOpen] = useState(false);

    return (
        <div className='sidebar-container'>
            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" class="w-6 h-6" height='100' width='100' 
onClick={() => setOpen(!open)}>
                <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
            </svg>
            {open &&
                <ul>
                    {color.map((item, index) => <li key={index} style={{ background: item }} onClick={() => props.addnotes(item)}></li>)}
                </ul>
            }
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Wow! What just happened. I know that's a lot to process. Don't worry I'll walk you through this. Oh! also keep your eyes open you might find some useful code snippet along the way that you use again and again almost in every project πŸ˜‰.

Now lets see the jsx first.

I did nothing fancy hear just created a parent div with class sidebar-container. See css above in starting. Then added a svg image to show icon. That's it.

If you look closely 🧐, you might see a onClick() function in svg tag. What does it do you might ask.

constt [open, setOpen] => useState(false);

<svg onClick ={() => setOpen(!open)}
Enter fullscreen mode Exit fullscreen mode

Well all it does it toggle the div or image or whatever you want to toggle. Like on/off the light. Think as you want something to render on screen only when click on a button and then disappear when you click again. Well this function do the trick. When you click on the button with which we attached this function, it toggles the state from true to false or vice versa.

Now your second question might be what the duck is

{open &&
                <ul>
                    {color.map((item, index) => <li key={index} style={{ background: item }} onClick={() => props.addnotes(item)}></li>)}
                </ul>
            }
Enter fullscreen mode Exit fullscreen mode

This is nothing but a way to tell the computer if state is true render that ul tag else do not render. This is called conditional rendering in world of react and && operator will be used to implement this magic. You can achieve this using class as shown below. Also I am using array of different colors which are mapped as a li list. This will used to produce notes component of different color.

<ul className={`sidebar_list ${open ? 'sidebar_list_active' : ''} `}>
                {
                    color.map((item, index) => <li className='sidebar_list_item' key={index} style={{ backgroundColor: item }} onClick={() => props.addNotes(item)} />)
                }
            </ul>
Enter fullscreen mode Exit fullscreen mode

You also need a pinch of css here to make it work. Note that classname are not same because it was copied from other project.


.sidebar {
    display: flex;
    flex-direction: column;
    gap: 40px;
}

.sidebar svg {
    cursor: pointer;
}

.sidebar_list {
    display: flex;
    flex-direction: column;
    gap: 20px;
    align-items: center;
    height: 0;
}

.sidebar_list_active {
    height: 300px;
}


.sidebar_list_item {
    list-style-type: none;
    height: 26px;
    width: 26px;
    border-radius: 50%;
}
Enter fullscreen mode Exit fullscreen mode

Wallah! Done and Dustin. See the preview below

Image description

Lets structure Notes component first

All I am doing is structure the jsx to see how it looks like. Remember it is very important to fist create the design and fill it with dummy data. It will be easier to imagine the solution.

import React from 'react'
import './Note.css'
function Note() {

    return (
        <div className='card-container' style={{ backgroundColor: 'cyan' }}>
            <textarea className='text-area' defaultValue={'asdasdsadas'}</textarea>
            <div className='footer'>
                <p> 16:26 PM 01 Nov</p>
                <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6" height='25' width='25'>
                    <path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
                </svg>
            </div>
        </div>
    )
}
export default Note
Enter fullscreen mode Exit fullscreen mode

This will show like this

Image description

Now that we see the design we can imagine how data will come in this jsx. Structure of data should be like below

{
text: 'ssasdasda',
time: '16:26 PM 01 Nov',
Enter fullscreen mode Exit fullscreen mode

Structure Note Container component

import React from 'react'
import Note from '../Note/Note'
import './NoteContainer.css'

export default function NoteContainer(props) {


    return (
        <div className='note-container'>
            <h1 className='header'>Notes</h1>
            <div className='note-body custom-scroll'>
                <Note/>
            </div>
        </div>
    )
}

Enter fullscreen mode Exit fullscreen mode

Notice that we are calling the Notes component in Note Container. All the notes will leave in Notes Container.

Now that the basic image is clear. We will work with dummy data and functionality along the way. I am gonna demonstrate how data travel through component in react. Remember , It is very important to understand props and states in react as it's the basic of mostly all react apps. I will also demonstrate how data propagate from children to parent in react. This is very important concept to learn. You must have clarity on callbacks in js.

Demonstration of props with dummy data


import './App.css';
import NoteContainer from './components/NoteContainer/NoteContainer';
import Sidebar from './components/Sidebar/Sidebar';
import { useState, useEffect } from 'react'

function App() {
const notes=[
 {
      text: 'vdscmdscdc',
      time: '4.37 PM',
      color: 'yellow'
    },
    {
      text: 'vdscmdscdc',
      time: '4.37 PM',
      color: 'pink'
    },
    {
      text: 'vdscmdscdc',
      time: '4.37 PM',
      color: 'orange'
    },
    {
      text: 'vdscmdscdc',
      time: '4.37 PM',
      color: 'seagreen'
    }]

  return (
    <div className="App">
      <Sidebar />
      <NoteContainer notes={notes} />
    </div>
  );
}

export default App;


//----notes array will send to note container as props---
import React from 'react'
import Note from '../Note/Note'
import './NoteContainer.css'

export default function NoteContainer(props) {

    return (
        <div className='note-container'>
            <h1 className='header'>Notes</h1>
            <div className='note-body custom-scroll'>
               <Note notes={props.notes} />
            </div>
        </div>
    )
}

//----from notes-container to notes data will travel---

import React from 'react'
import './Note.css'
function Note(props) {

   return (
        <div className='card-container' style={{ backgroundColor: props.notes.color }}>
            <textarea className='text-area' defaultValue= {props.notes.text}></textarea>
            <div className='footer'>
                <p> {props.notes.time}</p>
                <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6" height='25' width='25'>
                    <path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
                </svg>
            </div>
        </div>
    )
}
export default Note

Enter fullscreen mode Exit fullscreen mode

Functionality to create new notes

Image description

So we have to send signal from sidebar to notes component to create new note. App component will be the mediator. You will learn how child component send signals to parent component.

import './App.css';
import NoteContainer from './components/NoteContainer/NoteContainer';
import Sidebar from './components/Sidebar/Sidebar';
import { useState, useEffect } from 'react'

function App() {
const [notes, setNotes] = useState([
 {
      text: 'vdscmdscdc',
      time: '4.37 PM',
      color: 'yellow'
    },
    {
      text: 'vdscmdscdc',
      time: '4.37 PM',
      color: 'pink'
    },
    {
      text: 'vdscmdscdc',
      time: '4.37 PM',
      color: 'orange'
    },
    {
      text: 'vdscmdscdc',
      time: '4.37 PM',
      color: 'seagreen'
    }])

const addNotes = (color) => {
const tempNotes = [...notes];

tempNotes.push({
id: Date.now(),
text: '',
time: Date.now();
color,
})

setNotes(tempNotes);
}

  return (
    <div className="App">
      <Sidebar addNotes={addNotes}/>
      <NoteContainer notes={notes} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

We will get this function as props in sidebar component

Image description

Lets understand the functionality by an image

Image description

I'm hoping this image help you to understand what's going on here.
I'll first wrap this in bullets.

  • Create a add function in app component.
  • This component will return a object which will added to the main array of notes, which is stored in state.
  • This array is traveling to the Note container component and note container sending this data to Notes component.
  • Notes component fetching required data from array and feeding to the designated design field. I hope you get the idea.

Functionality to delete a note

Image description

Delete from ui is very simple. Believe it or not it just a one liner.

import './App.css';
import NoteContainer from './components/NoteContainer/NoteContainer';
import Sidebar from './components/Sidebar/Sidebar';
import { useState, useEffect } from 'react'

function App() {
const [notes, setNotes] = useState([
 {
      text: 'vdscmdscdc',
      time: '4.37 PM',
      color: 'yellow'
    },
    {
      text: 'vdscmdscdc',
      time: '4.37 PM',
      color: 'pink'
    },
    {
      text: 'vdscmdscdc',
      time: '4.37 PM',
      color: 'orange'
    },
    {
      text: 'vdscmdscdc',
      time: '4.37 PM',
      color: 'seagreen'
    }])

const addNotes = (color) => {
const tempNotes = [...notes];
tempNotes.push({
id: Date.now(),
text: '',
time: Date.now();
color,
})
setNotes(tempNotes);
}

const deleteNote = id => {
  //You can use two method to delete element
  // array filter method or array slice method
  tempNotes = [...notes];
  setNotes(tempNotes.filter(note => note.id !== id);
  /*
  using slicemethod
  const index = tempNotes.findIndex(note => note.id === id);
  if(!index) return;
  setNotes(tempNotes.slice(index,1));
  */
}

  return (
    <div className="App">
      <Sidebar addNotes={addNotes}/>
      <NoteContainer notes={notes} deleteNotes={deleteNotes}/>
    </div>
  );
}

//----------Note container will get delete function as props and send it to Notes component

import React from 'react'
import Note from '../Note/Note'
import './NoteContainer.css'

export default function NoteContainer(props) {

    return (
        <div className='note-container'>
            <h1 className='header'>Notes</h1>
            <div className='note-body custom-scroll'>
                {
                    props.notes.length > 0 ? notes.map((item) => (
                        <Note key={item.id} note={item} deleteNote={props.deleteNote}
                             />
                    )) : <h1>Empty Notes</h1>
                }
            </div>
        </div>
    )
}

//--------deleteNote function pass to the delete icon as callback function

import React from 'react'
import './Note.css'
function Note(props) {



    return (
        <div className='card-container' style={{ backgroundColor: props.note.color }}>
            <textarea className='text-area' defaultValue={props.note.text} onChange={e => updateText(e.target.value, props.note.id)}></textarea>
            <div className='footer'>
                <p>{props.note.time}</p>
                <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6" height='25' width='25' onClick={() => props.deleteNote(props.note.id)}>
                    <path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
                </svg>
            </div>
        </div>
    )
}
export default Note
Enter fullscreen mode Exit fullscreen mode

Also I added the conditional rendering in Note container component. If the length of the notes array is greater then 0 it will show notes else it will show 'Empty Notes' massage.

Image description

Persist Notes and text in local storage and update text

Also this includes format data, useEffect hooks, debounce **
**Plz write your queries in comments

//App

import './App.css';
import NoteContainer from './components/NoteContainer/NoteContainer';
import Sidebar from './components/Sidebar/Sidebar';
import { useState, useEffect } from 'react'

function App() {

  const [notes, setNotes] = useState(JSON.parse(localStorage.getItem('note-app')) || [])

  const addnotes = (color) => {
    const tempNotes = [...notes];
    tempNotes.push({
      id: Date.now() + '' + Math.floor(Math.random() * 78),
      text: '',
      time: Date.now(),
      color
    })

    setNotes(tempNotes);
  }

  const deleteNote = (id) => {
    const tempNotes = [...notes];
    // const index = tempNotes.findIndex(item => item.id === id);
    // if(index <=0) return
    //setNotes(tempNotes.splice(index,1))
    setNotes(tempNotes.filter(item => item.id !== id));
  }

  useEffect(() => {
    localStorage.setItem('note-app', JSON.stringify(notes));
  })

  const updateText = (text, id) => {
    const tempNotes = [...notes];
    const index = tempNotes.findIndex(item => item.id === id);
    if (index <= 0) return
    tempNotes[index].text = text;
    setNotes(tempNotes);
  }

  return (
    <div className="App">

      <Sidebar addnotes={addnotes} />
      <NoteContainer notes={notes} deleteNote={deleteNote} updateText={updateText} />

    </div>
  );
}
export default App;

//Sidebar
import React, { useState } from 'react'
import './Sidebar.css'

export default function Sidebar(props) {
    const color = ['#fe9b72', '#fec971', "#00d4fe", '#b693fd', '#e4ee91']
    const [open, setOpen] = useState(false);

    return (
        <div className='sidebar-container'>
            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" class="w-6 h-6" height='100' width='100' onClick={() => setOpen(!open)}>
                <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
            </svg>
            {open &&
                <ul>
                    {color.map((item, index) => <li key={index} style={{ background: item }} onClick={() => props.addnotes(item)}></li>)}
                </ul>
            }
        </div>
    )
}

//Note Container
import React from 'react'
import Note from '../Note/Note'
import './NoteContainer.css'

export default function NoteContainer(props) {

    const reverseArray = (arr) => {
        const array = [];
        for (let i = arr.length - 1; i >= 0; i--) {
            array.push(arr[i]);
        }
        return array
    }

    const notes = reverseArray(props.notes)

    return (
        <div className='note-container'>
            <h1 className='header'>Notes</h1>
            <div className='note-body custom-scroll'>
                {
                    notes.length > 0 ? notes.map((item) => (
                        <Note key={item.id} note={item} deleteNote={props.deleteNote}
                            updateText={props.updateText} />
                    )) : <h1>Empty Notes</h1>
                }
            </div>
        </div>
    )
}


// Notes

import React from 'react'
import './Note.css'
function Note(props) {

    const formatDate = (value) => {
        if (!value) return;
        const date = new Date(value);
        const monthsName = ['Jan', 'Feb', 'March', 'April', 'May', 'June', 'July', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec'];

        let hrs = date.getHours();
        let amPm = hrs > 12 ? 'PM' : 'AM';
        hrs = hrs ? hrs : '12';
        hrs = hrs > 24 ? hrs - 12 : hrs;

        let min = date.getMinutes();
        min = min < 10 ? '0' + min : min;

        let day = date.getDate();
        day = day < 10 ? '0' + day : day;
        const month = monthsName[date.getMonth()];

        return `${hrs}:${min} ${amPm} ${day} ${month}`;
    };

    let timer = 500, timeout;
    const debounce = (fun) => {
        clearTimeout(timeout);
        timeout = setTimeout(fun, timer);
    }


    const updateText = (text, id) => {
        debounce(() => props.updateText(text, id))
    };

    return (
        <div className='card-container' style={{ backgroundColor: props.note.color }}>
            <textarea className='text-area' defaultValue={props.note.text} onChange={e => updateText(e.target.value, props.note.id)}></textarea>
            <div className='footer'>
                <p>{formatDate(props.note.time)}</p>
                <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6" height='25' width='25' onClick={() => props.deleteNote(props.note.id)}>
                    <path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
                </svg>
            </div>
        </div>
    )
}
export default Note


Enter fullscreen mode Exit fullscreen mode

Latest comments (0)