Table of Contents
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:
- For creating a note (/).
- For displaying a note with the given id (/notes/:id).
- 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!