Your cart

Your cart is empty 🛒
Total items (unique) 0

Loading Article

Fetching content from our servers...

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:

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:

  1. Header: A simple, elegant top navigation bar.
  2. CreateNote: The input area where users type their titles and descriptions.
  3. 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 &lt;Note/&gt; 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:

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:

  1. Setting up modular React component systems.
  2. Handling forms, state mutations, and scope binding in class structures.
  3. 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:

Happy coding, and see you in the next tutorial!

Embedded resource: youtube