How to Build a Google Keep Clone with React.js (Class Components)
Learn how to build a fully functional Google Keep Clone using React.js class-based components. Master state management, component lifecycles, and localStorage data persistence in this ultimate beginner-friendly guide.
Every developer’s journey has a few rite-of-passage projects. Building a Todo list is usually the first. But if you really want to level up your frontend skills and understand how data flows through an application, building a Google Keep Clone is the ultimate next step.
In this comprehensive, step-by-step guide, we are going to build a fully functional Google Keep clone from scratch using React.js.
While modern React heavily features Functional Components and Hooks, master modern web development requires a rock-solid understanding of Class-Based Components and React Lifecycle Methods. Many massive enterprise codebases still rely on class components, and knowing how they work under the hood makes you a far more versatile developer.
By the end of this tutorial, you will have a sleek, responsive note-taking application complete with persistent data storage using the browser’s localStorage.
What We Will Build: Core Features
Before we dive into the code, let’s look at the core functionalities our Google Keep clone will feature:
- Dynamic UI Layout: A clean, responsive interface powered by Bootstrap, featuring an intuitive input form and neat note cards.
- State Management in Class Components: Utilizing constructors, initializing state, and correctly binding the this keyword.
- React Lifecycle Methods: Mastering componentDidMount and componentDidUpdate to handle real-time data persistence.
- Full CRUD Functionality: Adding new notes, dynamically rendering them using JavaScript array methods, and deleting them seamlessly.
- Persistent Storage: Syncing our React state with the browser's localStorage so users never lose their notes on a page refresh.
Step 1: Project Setup and UI Architecture
First things first: let's get our environment ready. We will start by creating a fresh React application and integrating Bootstrap to keep our styling quick and highly responsive.
Initializing the Project
Open your terminal, navigate to your development directory, and run the following commands:
Bash
npx create-react-app google-keep-clone
cd google-keep-clone
npm install bootstrap
Structuring Our Components
A clean project layout makes debugging a breeze. Inside your src directory, create a folder named components. We will break our application down into three primary components:
- Header: A simple, elegant top navigation bar.
- CreateNote: The input area where users type their titles and descriptions.
- Note: The individual card component that displays the saved note.
To include Bootstrap's styling across the entire project, open your src/index.js file and add this import statement at the top:
JavaScript
import 'bootstrap/dist/css/bootstrap.min.css';
Step 2: Deep Dive into Class-Based Components
Now that our boilerplate is set up, let's look at the backbone of this project: React Class Components.
Unlike functional components that use the useState hook, class components use a JavaScript class that extends React.Component. They rely on a constructor to initialize state and require explicit binding of methods so that the this keyword refers to the component instance.
Here is how we initialize our main App.js component:
JavaScript
import React, { Component } from 'react';
import Header from './components/Header';
import CreateNote from './components/CreateNote';
import Note from './components/Note';
class App extends Component {
constructor(props) {
super(props);
// Initializing the state
this.state = {
notes: []
};
// Binding methods to ensure 'this' context is preserved
this.addNote = this.addNote.bind(this);
this.deleteNote = this.deleteNote.bind(this);
}
render() {
return (
<div className="app-container">
<Header />
<div className="container my-5">
<CreateNote onAdd={this.addNote} />
{/* Notes will be rendered here */}
</div>
</div>
);
}
}
export default App;
Pro-Tip: Demystifying this
In JavaScript, the value of this is determined by how a function is called. In React class components, if you pass a method down to a child component (like passing this.addNote to CreateNote) without binding it in the constructor, this will return undefined when the function executes. Always remember to bind your custom methods!
Step 3: Mastering React Lifecycle Methods
One of the most crucial concepts in class-based React development is the component lifecycle. Every component goes through a cycle of creation (mounting), growth (updating), and death (unmounting).
To make our application functional even after a user closes their browser tab, we will leverage two essential lifecycle methods: componentDidMount and componentDidUpdate.
1. Fetching Data with componentDidMount
This method runs exactly once immediately after the component is rendered into the DOM. It is the perfect place to fetch external data or pull existing items out of localStorage.
JavaScript
componentDidMount() {
try {
const savedNotes = localStorage.getItem('keep_notes');
if (savedNotes) {
this.setState({ notes: JSON.parse(savedNotes) });
}
} catch (error) {
console.error("Error loading notes from localStorage:", error);
}
}
Why use JSON.parse? localStorage can only store data as raw strings. When we pull our data out, we must convert that string back into a readable JavaScript array using JSON.parse().
2. Synchronizing Data with componentDidUpdate
This method fires automatically whenever a component's state or props change. We can use it to listen for changes in our notes array. Whenever a note is added or removed, we instantly update localStorage.
JavaScript
componentDidUpdate(prevProps, prevState) {
// Always compare the current state to the previous state to avoid infinite loops!
if (prevState.notes !== this.state.notes) {
localStorage.setItem('keep_notes', JSON.stringify(this.state.notes));
}
}
Step 4: Implementing CRUD Operations
With our architecture and state management firmly established, it is time to build the actual note-taking mechanics.
1. Creating New Notes (CreateNote.js)
The CreateNote component needs to capture user input for both the title and the note content, hold that temporary data in its local state, and then pass it back up to App.js when the user clicks submit.
Here is the implementation of the form handling:
JavaScript
import React, { Component } from 'react';
class CreateNote extends Component {
constructor(props) {
super(props);
this.state = {
title: "",
content: ""
};
this.handleChange = this.handleChange.bind(this);
this.submitNote = this.submitNote.bind(this);
}
handleChange(event) {
const { name, value } = event.target;
this.setState({ [name]: value });
}
submitNote(event) {
event.preventDefault();
if (!this.state.title && !this.state.content) return; // Prevent empty notes
this.props.onAdd(this.state);
this.setState({ title: "", content: "" }); // Reset input fields
}
render() {
return (
<div className="card p-3 shadow-sm mx-auto my-4" style={{ maxWidth: "500px" }}>
<form onSubmit={this.submitNote}>
<input
type="text"
name="title"
placeholder="Title"
className="form-control mb-2 border-0 fw-bold shadow-none"
value={this.state.title}
onChange={this.handleChange}
/>
<textarea
name="content"
placeholder="Take a note..."
rows="3"
className="form-control border-0 shadow-none"
value={this.state.content}
onChange={this.handleChange}
/>
<button type="submit" className="btn btn-warning float-end rounded-circle mt-2 shadow-sm">
+
</button>
</form>
</div>
);
}
}
export default CreateNote;
Back in App.js, we handle the incoming new note object and append it to our state array securely without modifying the original state directly:
JavaScript
addNote(newNote) {
this.setState(prevState => {
return { notes: [...prevState.notes, newNote] };
});
}
2. Dynamically Rendering the Notes
To show our list of notes on screen, we use the JavaScript .map() function. This maps over our notes state array and converts each raw object into a visual <Note/> card component.
Inside the render() method of App.js:
JavaScript
<div className="row">
{this.state.notes.map((noteItem, index) => {
return (
<div className="col-md-4 my-3" key={index}>
<Note
id={index}
title={noteItem.title}
content={noteItem.content}
onDelete={this.deleteNote}
/>
</div>
);
})}
</div>
3. Deleting Notes by Index
To delete a note, we pass its index back to the parent component. We then use the .filter() method to create a brand new array that excludes the note matching that specific index.
JavaScript
deleteNote(id) {
this.setState(prevState => {
return {
notes: prevState.notes.filter((noteItem, index) => {
return index !== id;
})
};
});
}
And here is our lightweight, clean Note.js component:
JavaScript
import React, { Component } from 'react';
class Note extends Component {
render() {
return (
<div className="card h-100 shadow-sm">
<div className="card-body d-flex flex-column justify-content-between">
<div>
<h5 className="card-title fw-bold">{this.props.title}</h5>
<p className="card-text text-secondary" style={{ whiteSpace: "pre-wrap" }}>
{this.props.content}
</p>
</div>
<button
className="btn btn-outline-danger btn-sm align-self-end mt-3 border-0"
onClick={() => this.props.onDelete(this.props.id)}
>
Delete
</button>
</div>
</div>
);
}
}
export default Note;
Step 5: Common Pitfalls and Troubleshooting
When building class-based applications, even seasoned developers run into standard architectural hiccups. Keep an eye out for these errors:
- State Mutation Bugs: Never write this.state.notes.push(newNote). React cannot track direct mutations, meaning your UI will not re-render. Always use this.setState().
- Infinite Component Updates: If you run this.setState() inside componentDidUpdate without wrapping it in a conditional check (comparing prevState to this.state), your application will get stuck in an infinite rendering loop and crash.
- Stringification Errors: Trying to save raw arrays directly to localStorage will result in the string "[object Object]". Always run data through JSON.stringify() before saving.
Conclusion: Expanding the Project
Congratulations! You have successfully built a fully operational Google Keep Clone using React class components and persistent browser storage.
Through this project, you have mastered:
- Setting up modular React component systems.
- Handling forms, state mutations, and scope binding in class structures.
- Leveraging component lifecycles to sync runtime application memory with localStorage.
Where should you go from here? If you want to push this project further, try adding these features:
- Search Functionality: Add an input bar in the header to filter notes by title keywords.
- Color Themes: Allow users to change the background color of individual notes just like in the real Google Keep app.
- Edit Functionality: Allow users to double-click a note card to update its content directly.
Happy coding, and see you in the next tutorial!
Embedded resource: youtube