🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Build a complete React notes application
- Use React Hooks for state management
- Implement CRUD operations for notes
- Integrate with local storage
- Create reusable components
- Build a responsive user interface
📚 Project Overview
We'll build a notes application that includes: - Create, read, update, delete notes - Local storage persistence - Search and filter functionality - Responsive design - Component reusability
🏗️ Project Structure
notes-app/
├── package.json
├── public/
│ └── index.html
├── src/
│ ├── App.js
│ ├── index.js
│ ├── components/
│ │ ├── NoteCard.js
│ │ ├── NoteEditor.js
│ │ ├── SearchBar.js
│ │ └── NoteList.js
│ └── hooks/
│ └── useNotes.js
└── styles/
└── App.css
💻 Complete Implementation
App.js
import React, { useState, useEffect } from 'react';
import './styles/App.css';
import NoteEditor from './components/NoteEditor';
import NoteList from './components/NoteList';
import SearchBar from './components/SearchBar';
import { useNotes } from './hooks/useNotes';
function App() {
const [searchTerm, setSearchTerm] = useState('');
const [selectedNote, setSelectedNote] = useState(null);
const [isEditing, setIsEditing] = useState(false);
const {
notes,
loading,
addNote,
updateNote,
deleteNote,
searchNotes
} = useNotes();
const filteredNotes = searchTerm
? searchNotes(searchTerm)
: notes;
const handleNoteSelect = (note) => {
setSelectedNote(note);
setIsEditing(false);
};
const handleEditNote = (note) => {
setSelectedNote(note);
setIsEditing(true);
};
const handleSaveNote = (noteData) => {
if (isEditing && selectedNote) {
updateNote(selectedNote.id, noteData);
} else {
addNote(noteData);
}
setSelectedNote(null);
setIsEditing(false);
};
const handleDeleteNote = (noteId) => {
if (window.confirm('Are you sure you want to delete this note?')) {
deleteNote(noteId);
if (selectedNote && selectedNote.id === noteId) {
setSelectedNote(null);
setIsEditing(false);
}
}
};
const handleNewNote = () => {
setSelectedNote(null);
setIsEditing(true);
};
return (
<div className="app">
<header className="app-header">
<h1>Notes App</h1>
<button className="new-note-btn" onClick={handleNewNote}>
New Note
</button>
</header>
<main className="app-main">
<div className="sidebar">
<SearchBar
value={searchTerm}
onChange={setSearchTerm}
placeholder="Search notes..."
/>
<NoteList
notes={filteredNotes}
selectedNote={selectedNote}
onNoteSelect={handleNoteSelect}
onEditNote={handleEditNote}
onDeleteNote={handleDeleteNote}
/>
</div>
<div className="main-content">
{selectedNote || isEditing ? (
<NoteEditor
note={selectedNote}
isEditing={isEditing}
onSave={handleSaveNote}
onCancel={() => {
setSelectedNote(null);
setIsEditing(false);
}}
/>
) : (
<div className="welcome">
<h2>Welcome to Notes App</h2>
<p>Select a note from the sidebar or create a new one.</p>
</div>
)}
</div>
</main>
</div>
);
}
export default App;
hooks/useNotes.js
import { useState, useEffect, useCallback } from 'react';
export function useNotes() {
const [notes, setNotes] = useState([]);
const [loading, setLoading] = useState(true);
// Load notes from localStorage on mount
useEffect(() => {
const savedNotes = localStorage.getItem('notes');
if (savedNotes) {
try {
setNotes(JSON.parse(savedNotes));
} catch (error) {
console.error('Error loading notes:', error);
}
}
setLoading(false);
}, []);
// Save notes to localStorage whenever notes change
useEffect(() => {
if (!loading) {
localStorage.setItem('notes', JSON.stringify(notes));
}
}, [notes, loading]);
const addNote = useCallback((noteData) => {
const newNote = {
id: Date.now(),
title: noteData.title || 'Untitled',
content: noteData.content || '',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
tags: noteData.tags || []
};
setNotes(prev => [newNote, ...prev]);
return newNote;
}, []);
const updateNote = useCallback((id, noteData) => {
setNotes(prev => prev.map(note =>
note.id === id
? {
...note,
...noteData,
updatedAt: new Date().toISOString()
}
: note
));
}, []);
const deleteNote = useCallback((id) => {
setNotes(prev => prev.filter(note => note.id !== id));
}, []);
const searchNotes = useCallback((searchTerm) => {
if (!searchTerm.trim()) return notes;
const term = searchTerm.toLowerCase();
return notes.filter(note =>
note.title.toLowerCase().includes(term) ||
note.content.toLowerCase().includes(term) ||
note.tags.some(tag => tag.toLowerCase().includes(term))
);
}, [notes]);
const getNoteById = useCallback((id) => {
return notes.find(note => note.id === id);
}, [notes]);
return {
notes,
loading,
addNote,
updateNote,
deleteNote,
searchNotes,
getNoteById
};
}
components/NoteEditor.js
import React, { useState, useEffect } from 'react';
function NoteEditor({ note, isEditing, onSave, onCancel }) {
const [formData, setFormData] = useState({
title: '',
content: '',
tags: []
});
const [tagInput, setTagInput] = useState('');
useEffect(() => {
if (note) {
setFormData({
title: note.title || '',
content: note.content || '',
tags: note.tags || []
});
} else {
setFormData({
title: '',
content: '',
tags: []
});
}
}, [note]);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleAddTag = (e) => {
if (e.key === 'Enter' && tagInput.trim()) {
e.preventDefault();
const newTag = tagInput.trim();
if (!formData.tags.includes(newTag)) {
setFormData(prev => ({
...prev,
tags: [...prev.tags, newTag]
}));
}
setTagInput('');
}
};
const handleRemoveTag = (tagToRemove) => {
setFormData(prev => ({
...prev,
tags: prev.tags.filter(tag => tag !== tagToRemove)
}));
};
const handleSave = (e) => {
e.preventDefault();
onSave(formData);
};
const handleCancel = () => {
onCancel();
};
return (
<div className="note-editor">
<form onSubmit={handleSave}>
<div className="editor-header">
<input
type="text"
name="title"
value={formData.title}
onChange={handleChange}
placeholder="Note title..."
className="title-input"
/>
<div className="editor-actions">
<button type="button" onClick={handleCancel}>
Cancel
</button>
<button type="submit">
{isEditing ? 'Update' : 'Save'}
</button>
</div>
</div>
<div className="tags-section">
<div className="tags-input">
<input
type="text"
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyPress={handleAddTag}
placeholder="Add tags..."
/>
</div>
<div className="tags-list">
{formData.tags.map(tag => (
<span key={tag} className="tag">
{tag}
<button
type="button"
onClick={() => handleRemoveTag(tag)}
className="tag-remove"
>
×
</button>
</span>
))}
</div>
</div>
<textarea
name="content"
value={formData.content}
onChange={handleChange}
placeholder="Write your note here..."
className="content-textarea"
/>
</form>
</div>
);
}
export default NoteEditor;
components/NoteList.js
import React from 'react';
import NoteCard from './NoteCard';
function NoteList({ notes, selectedNote, onNoteSelect, onEditNote, onDeleteNote }) {
if (notes.length === 0) {
return (
<div className="note-list">
<div className="empty-state">
<p>No notes found</p>
<p>Create your first note!</p>
</div>
</div>
);
}
return (
<div className="note-list">
{notes.map(note => (
<NoteCard
key={note.id}
note={note}
isSelected={selectedNote && selectedNote.id === note.id}
onSelect={onNoteSelect}
onEdit={onEditNote}
onDelete={onDeleteNote}
/>
))}
</div>
);
}
export default NoteList;
components/NoteCard.js
import React from 'react';
function NoteCard({ note, isSelected, onSelect, onEdit, onDelete }) {
const handleClick = () => {
onSelect(note);
};
const handleEdit = (e) => {
e.stopPropagation();
onEdit(note);
};
const handleDelete = (e) => {
e.stopPropagation();
onDelete(note.id);
};
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString();
};
const truncateContent = (content, maxLength = 100) => {
if (content.length <= maxLength) return content;
return content.substring(0, maxLength) + '...';
};
return (
<div
className={`note-card ${isSelected ? 'selected' : ''}`}
onClick={handleClick}
>
<div className="note-header">
<h3 className="note-title">{note.title}</h3>
<div className="note-actions">
<button
className="edit-btn"
onClick={handleEdit}
title="Edit note"
>
✏️
</button>
<button
className="delete-btn"
onClick={handleDelete}
title="Delete note"
>
🗑️
</button>
</div>
</div>
<div className="note-content">
<p>{truncateContent(note.content)}</p>
</div>
<div className="note-footer">
<div className="note-tags">
{note.tags.map(tag => (
<span key={tag} className="tag">{tag}</span>
))}
</div>
<div className="note-date">
{formatDate(note.updatedAt)}
</div>
</div>
</div>
);
}
export default NoteCard;
🎯 Key Features Implemented
- CRUD Operations - Create, read, update, delete notes
- Local Storage - Persist notes between sessions
- Search Functionality - Find notes by title, content, or tags
- Tag System - Organize notes with tags
- Responsive Design - Works on all devices
- Component Reusability - Modular component structure
- State Management - React Hooks for state
🎯 Key Takeaways
- React Hooks manage component state and side effects
- Local storage persists data between sessions
- Component composition builds complex UIs from simple parts
- CRUD operations handle data manipulation
- Search functionality improves user experience
🚀 Next Steps
Congratulations! You've completed Module 7. You now understand:
- Frontend frameworks and their purposes
- React.js fundamentals and setup
- JSX syntax and component creation
- Props and state management
- Event handling in React
- Conditional rendering and lists
- React Hooks for state management
- Routing and navigation
- Building complete React applications
Ready for Module 8: Capstone Projects & Final Topics! 🎉
Next Module: Module 8: Capstone Projects & Final Topics