The Note-Taking App Project in React (Part 2)

You must first complete Getting Started with React before viewing this Lesson

This is the second part of the project “Creating a Note-Taking App Project in React”. In the first part, we organized our project and set up routers and the user interface. Here we will create context and connect it to NotePage and MyNotes page components. We will do the work of logic and functionality for the app.

Creating The Note Context

We have not stored any notes yet. For that, we will use the Context API of React. Using the context API, the data shared by the provider becomes available to any descendant component.

Create a note context object inside a contexts folder and add the code:

src/contexts/note-context.js

import { createContext } from 'react'
const NoteContext = createContext({})
export default NoteContext

Now, let’s use this context to create Providers and Consumers.

Create the Note Context Provider

The provider component is a wrapper for the components to share the notes state with other descendant components. We can also use the provider to share helper methods like addNote(), removeNote(), and updateNote(). These methods are required to update the notes array from any component.

The code for NoteContext Provider includes logic to update notes state and it shares the data with its children.

src/contexts/note-context.provider.jsx

import React, {
    Component
} from 'react'
import NoteContext from './note-context'
class NoteProvider extends Component {
    constructor() {
        super()
        this.state = {
            notes: [],
        }
    }
    getNote = id => {
        /* get a note from the notes state by id */
        const note = this.state.notes.find(note => note.id === Number(id))
        console.log('getnote id', id, note, this.state.notes)
        return note
    }
    addNote = (title, text) => {
        /* add a note to the notes state */
        this.setState(state => {
            const newNote = {
                id: state.notes.length + 1,
                title,
                text
            }
            return {
                notes: [...state.notes, newNote],
            }
        })
    }
    updateNote = note => {
        /* update a note in the notes with the matching id */
        const notes = [...this.state.notes]
        const noteIndex = notes.findIndex(n => n.id === note.id)
        if (noteIndex !== -1) {
            notes[noteIndex] = note
            this.setState({
                notes
            })
        }
    }
    removeNote = id => {
        /* remove a note from the notes state by id */
        this.setState(state => ({
            notes: state.notes.filter(note => note.id !== id),
        }))
    }
    render() {
        /* data and methods to expose to child components */
        const contextValue = {
            /* share the notes state and methods */
            notes: this.state.notes,
            addNote: this.addNote,
            updateNote: this.updateNote,
            removeNote: this.removeNote,
            getNote: this.getNote,
        }
        return ( <
            NoteContext.Provider value = {
                contextValue
            } > {
                this.props.children
            } 
            </NoteContext.Provider>
        )
    }
}
export default NoteProvider

The Provider component is a property of the context object. It takes a context value to share with all the descendant components of the provider.

Since we are using  {this.props.children}, any child that is wrapped inside the <NoteProvider /> will be rendered as a descendant.

Now wrap the components in App that need the NoteProvider.

src/App.js

import NoteProvider from './contexts/note-context.provider'
// inside render…
    <div className='App'>
        <NoteProvider>
          {/* Now, any descendant like NotePage, NoteItem can use the values shared by the note provider such as notes, addNote(), removeNote(), updateNote(), etc. */}
            <Header />
            <Router />
        </NoteProvider>
    </div>

Now all the functionality and core logic is ready for use. We need to connect and consume it inside the child components.

Saving Notes to the NoteContext

To save notes to the context object Provider, we need to consume the context and use the shared methods(addNote) to do that.

Update the note component by using contexts.

src/pages/note/note.component.jsx

import React, {
    useContext,
    useState
} from 'react'
import NoteContext from '../../contexts/note-context'
import './note.styles.css'
const NotePage = props => {
    const [title, setTitle] = useState('')
    const [text, setText] = useState('')
    /* the NoteContext consumes the value shared by the Provider */
    const {
        addNote
    } = useContext(NoteContext)
    const handleSubmit = e => {
        /* call the addNote method from the note context */
        addNote(title, text)
        /* redirect the user to /notes after saving them */
        history.push('/notes')
    }
    /*** …previous rendering code… ***/
}
export default NotePage

Displaying Notes from the NoteContext

We can display the notes by consuming the context in MyNotes page component. Then we can render NoteItem for each note object in the notes array.

Rewrite the MyNotes page component to use notes array from the context:

src/mynotes/mynotes.component.jsx

import React, {
    Component,
    useContext
} from 'react'
import NoteItem from '../../components/note-item/note-item.component'
import NoteContext from '../../contexts/note-context'
import './mynotes.styles.css'

function MyNotes() {
    /* get the notes and removeNote() method from NoteContext */
    const {
        notes,
        removeNote
    } = useContext(NoteContext)
    return ( <div>
        <h1> My Notes < /h1> 
            <div className = 'note-container' > {
            /* use map to render each note object of the array as a component. */ } {
            notes.map(note => ( <
                NoteItem key = {
                    note.id
                }
                note = {
                    note
                }
                removeNote = {
                    removeNote
                }
                />
            ))
        } </div> 
        </div>
    )
}
export default MyNotes

We have passed the note object to the NoteItem component together with the removeNote method. All of these will be available as props for the NoteItem component.

Any note saved from the Note page should be displayed in the MyNotes page as they are stored in the context.

Adding Functionality to <NoteItem />

The edit and delete buttons inside the NoteItem are not functional right now. We have to add onClick event listeners and perform edit/delete operations.

  • When the edit button is clicked, we will redirect the user to the edit page using history.push().
  • For the delete button, we will use the removeNote() method that we passed as props from the MyNotes component.

Code for NoteItem with button functionality:

src/components/note-item/note-item.component.jsx

import './note-item.styles.css'
import { withRouter } from 'react-router-dom'
const NoteItem = props => {
    const { note, history, removeNote } = props
    const { id, title, text } = note
    const openNote = () => {
        /* redirect the user to /notes/:id on clicking the note */
        /* pass the note id to dynamic route with params */
        history.push('/notes/${id}')
    }
    const editNote = e => {
/* redirect the user to /notes/edit/:id on clicking edit button*/
        e.stopPropagation()
        /* pass the note id to dynamic route with params */
        history.push('/notes/edit/${id}')
    }
    return (
        <div className='note-item' onClick={openNote}>
            <div className='note-title'>{id}. {title}</div>
            <div className='note-btn-container'>
<button className='note-btn edit-btn' onClick={editNote}>
                    Edit
                </button>
                <button
                    className='note-btn delete-btn'
                    onClick={e => {
/* use removeNote() on clicking delete button */
                        e.stopPropagation()
                        removeNote(id)
                    }}>
                    Delete
                </button>
            </div>
        </div>
    )
}
NoteItem.defaultProps = { note: { id: '', title: '', text: '' } }
export default withRouter(NoteItem)

We have wrapped <NoteItem /> using withRouter() so that we can access router properties like “history” for redirecting the user.

Now <NoteItem /> can delete notes and open edit page.

Edit Note and Show Note pages

We will reuse the same <Note /> page component to edit notes and to display a single note.

First, update the Router component and use the <NotePage /> component for “/notes/:id” and “/notes/edit/:id” paths.

src/components/router/router.component.jsx

{
    /* render the same <NotePage /> component as in "/" route */ } 
    <Route path = '/notes/:id' exact > {
        /* pass a showNote prop to tell the component to display a note with the given id*/ } 
        <NotePage showNote />
    </Route> 
    <Route path = '/notes/edit/:id' > {
        /* pass an editNote prop to make NotePage render an editing UI */ } 
        <NotePage editNote />
    </Route>

Customizing Note page to allow note editing and to show a note

In the last two routes, we have passed showNote and editNote props. When we pass a prop without any value, it has a true value by default which we can use in our component to render different UI.

So, we are reusing the Note page for three different purposes:

  1. For creating a note (/).
  2. For displaying a note with the given id (/notes/:id).
  3. For editing a note with the given id (/notes/edit/:id).

Here is the updated code for NotePage.

src/pages/note/note.component.jsx

cimport React, {
    useContext,
    useState,
    useEffect
}
from 'react' {
    /* previous imports */ }
const NotePage = props => {
    const [id, setId] = useState('')
    const [title, setTitle] = useState('')
    const [text, setText] = useState('')
    /* get all the required methods from the context */
    const {
        addNote,
        updateNote,
        getNote
    } = useContext(NoteContext)
    const {
        showNote,
        editNote,
        match: {
            params
        },
        history
    } = props
    const noteId = params.id
    useEffect(() => {
        /* use effect will run only once as componentDidMount because its second argument is passed an empty array */
        /* The purpose of this function is to – 
        -> get the note object from the context using the id param. 
        -> store the retrieved note object inside the component’s state.
        */
        if (noteId) {
            const note = getNote(noteId)
            if (note) {
                setId(note.id)
                setTitle(note.title)
                setText(note.text)
            }
        }
    }, [])
    const handleSubmit = e => {
        e.preventDefault()
        /* editNote is passed as a prop from the Router component for /notes/edit/:id paths */
        if (editNote) {
            const note = {
                id,
                title,
                text,
            }
            updateNote(note)
        } else {
            addNote(title, text)
        }
        history.push('/notes')
    }
    /* If the showNote is passed to <NotePage /> in our Router code, then return just the text and the title. */
    if (showNote) {
        return ( <div>
            <h1> {
                title
            } </h1> 
                <p> {
                text
            } </p> 
                </div>
        )
    }
    return ( <div>
        <h1> {
            editNote ? 'Edit Note' : 'Create a note'
        } </h1> 
            <form className = 'form'
        onSubmit = {
            handleSubmit
        } > {
            /* input and text area code */ } 
        <input type = 'submit'
        className = 'form-submit'
        value = {
            editNote ? 'Save Changes' : 'Save Note'
        }/> 
          </form> 
        </div>
    )
}
export default withRouter(NotePage)

Complete Source Code

The source code for the Note-taking app is available for download from the link below. Make sure to run “npm install” to set up all the dependencies. Then you can run “npm start” to compile and view the output.

Download the “Note Taking App” Source Code

Summary

The Note-Taking App covered many important topics. Notable ones include:

  • Components, Props, and State
  • Routing with react-router
  • Context API, Providers, and Consumers
  • React Hooks – useState, useEffect, useContext

We have also learned the proper way to structure folders for components and pages. Each file/component should be small and concise. This helps to create manageable projects.

It would be best to try to create other apps using the same pattern you learned from this code. Good Luck!

Back to: React Tutorial > React Projects

Leave a Reply

Your email address will not be published. Required fields are marked *