Build a Simple Next.js Notes App: A Beginner’s Guide

Written by

in

In the ever-evolving world of web development, staying current with modern frameworks is crucial. Next.js, a React framework, has gained significant popularity for its server-side rendering, static site generation, and overall developer experience. This guide will walk you through building a simple Notes application using Next.js, perfect for beginners and intermediate developers looking to solidify their understanding of the framework. We’ll cover everything from setting up your project to implementing features like creating, reading, updating, and deleting notes (CRUD operations). By the end, you’ll have a functional application and a solid grasp of core Next.js concepts.

Why Build a Notes App?

A Notes application, while seemingly simple, provides an excellent platform for learning fundamental web development concepts. It allows you to practice:

  • State Management: Managing the notes’ data and how it changes.
  • User Interface (UI) Design: Creating a user-friendly and intuitive interface.
  • Data Persistence: Storing and retrieving data, even if it’s just locally.
  • CRUD Operations: Mastering the Create, Read, Update, and Delete actions.
  • Routing: Navigating between different parts of your application.

Building a notes app is a manageable project, making it ideal for learning and experimenting with Next.js. You’ll gain practical experience that can be applied to more complex projects.

Prerequisites

Before we begin, ensure you have the following installed on your system:

  • Node.js and npm: Essential for running JavaScript code and managing project dependencies.
  • A Code Editor: Such as Visual Studio Code, Sublime Text, or Atom.
  • Basic Understanding of: HTML, CSS, and JavaScript. Knowledge of React is beneficial but not strictly required.

Setting Up Your Next.js Project

Let’s start by creating a new Next.js project. Open your terminal and run the following command:

npx create-next-app my-notes-app

This command will create a new directory called `my-notes-app` with all the necessary files to get you started. Navigate into the project directory:

cd my-notes-app

Now, start the development server:

npm run dev

Open your browser and go to `http://localhost:3000`. You should see the default Next.js welcome page.

Project Structure

Let’s take a quick look at the project structure. The key directories and files we’ll be working with are:

  • `pages/`: This directory contains your application’s pages. Each file in this directory represents a route. For example, `pages/index.js` is the home page, and `pages/about.js` would be the about page.
  • `components/`: We’ll create this directory to store reusable React components.
  • `styles/`: This directory is where you’ll put your CSS or other styling files.
  • `public/`: This directory is for static assets like images.
  • `package.json`: This file lists your project’s dependencies and scripts.

Creating the Note Component

First, we’ll create a component to display each individual note. Inside the `components` directory, create a file named `Note.js`. Add the following code:

// components/Note.js
import React from 'react';

const Note = ({ note, onDelete, onEdit }) => {
  return (
    <div>
      <h3>{note.title}</h3>
      <p>{note.content}</p>
      <button> onEdit(note.id)}>Edit</button>
      <button> onDelete(note.id)}>Delete</button>
    </div>
  );
};

export default Note;

This component takes a `note` object as a prop, which contains the title and content of the note. It also receives `onDelete` and `onEdit` functions, which will be used to handle deleting and editing notes.

Building the Notes List Page

Now, let’s modify the `pages/index.js` file to display a list of notes. Replace the existing content with the following:

// pages/index.js
import React, { useState } from 'react';
import Note from '../components/Note';

const Home = () => {
  const [notes, setNotes] = useState([
    { id: 1, title: 'Grocery List', content: 'Milk, eggs, bread' },
    { id: 2, title: 'Meeting Notes', content: 'Discuss project progress' },
  ]);

  const handleDeleteNote = (id) => {
    setNotes(notes.filter((note) => note.id !== id));
  };

  const handleEditNote = (id) => {
    // Implement edit logic here
    alert(`Editing note with ID: ${id}`);
  };

  return (
    <div>
      <h1>My Notes</h1>
      {notes.map((note) => (
        
      ))}
    </div>
  );
};

export default Home;

In this code:

  • We import the `Note` component.
  • We use the `useState` hook to manage an array of `notes`. Initially, we provide some sample notes.
  • The `handleDeleteNote` function filters the `notes` array to remove the note with the specified ID.
  • The `handleEditNote` function is a placeholder for now. We’ll implement edit functionality later.
  • We map over the `notes` array and render a `Note` component for each note.

Adding Basic Styling

To make the application look a bit better, let’s add some basic styling. Open `styles/globals.css` and add the following CSS:

/* styles/globals.css */
body {
  font-family: sans-serif;
  margin: 20px;
}

h1 {
  margin-bottom: 20px;
}

.note {
  border: 1px solid #ccc;
  padding: 10px;
  margin-bottom: 10px;
}

button {
  margin-right: 10px;
  padding: 5px 10px;
  cursor: pointer;
}

This CSS provides basic styling for the body, headings, notes, and buttons. You can customize this to your liking.

Implementing Note Creation

Let’s add a form to create new notes. Modify `pages/index.js` again:

// pages/index.js
import React, { useState } from 'react';
import Note from '../components/Note';

const Home = () => {
  const [notes, setNotes] = useState([
    { id: 1, title: 'Grocery List', content: 'Milk, eggs, bread' },
    { id: 2, title: 'Meeting Notes', content: 'Discuss project progress' },
  ]);
  const [newNoteTitle, setNewNoteTitle] = useState('');
  const [newNoteContent, setNewNoteContent] = useState('');

  const handleTitleChange = (e) => {
    setNewNoteTitle(e.target.value);
  };

  const handleContentChange = (e) => {
    setNewNoteContent(e.target.value);
  };

  const handleAddNote = () => {
    if (newNoteTitle.trim() === '' || newNoteContent.trim() === '') {
      alert('Please fill in both title and content.');
      return;
    }

    const newNote = {
      id: Date.now(), // Simple unique ID
      title: newNoteTitle,
      content: newNoteContent,
    };
    setNotes([...notes, newNote]);
    setNewNoteTitle('');
    setNewNoteContent('');
  };

  const handleDeleteNote = (id) => {
    setNotes(notes.filter((note) => note.id !== id));
  };

  const handleEditNote = (id) => {
    // Implement edit logic here
    alert(`Editing note with ID: ${id}`);
  };

  return (
    <div>
      <h1>My Notes</h1>
      <div>
        
        
        <button>Add Note</button>
      </div>
      {notes.map((note) => (
        
      ))}
    </div>
  );
};

export default Home;

Here’s what’s new:

  • We added state variables for `newNoteTitle` and `newNoteContent` to store the input values.
  • `handleTitleChange` and `handleContentChange` update the state when the input fields change.
  • `handleAddNote` creates a new note object, adds it to the `notes` array, and clears the input fields. It also includes basic validation.
  • We added input fields and a button for adding new notes.

Implementing Note Editing

Now, let’s implement the edit functionality. This involves displaying a form pre-filled with the note’s data, allowing the user to make changes, and then updating the note. Modify `pages/index.js` again:

// pages/index.js
import React, { useState } from 'react';
import Note from '../components/Note';

const Home = () => {
  const [notes, setNotes] = useState([
    { id: 1, title: 'Grocery List', content: 'Milk, eggs, bread' },
    { id: 2, title: 'Meeting Notes', content: 'Discuss project progress' },
  ]);
  const [newNoteTitle, setNewNoteTitle] = useState('');
  const [newNoteContent, setNewNoteContent] = useState('');
  const [editingNoteId, setEditingNoteId] = useState(null);
  const [editNoteTitle, setEditNoteTitle] = useState('');
  const [editNoteContent, setEditNoteContent] = useState('');

  const handleTitleChange = (e) => {
    setNewNoteTitle(e.target.value);
  };

  const handleContentChange = (e) => {
    setNewNoteContent(e.target.value);
  };

  const handleAddNote = () => {
    if (newNoteTitle.trim() === '' || newNoteContent.trim() === '') {
      alert('Please fill in both title and content.');
      return;
    }

    const newNote = {
      id: Date.now(), // Simple unique ID
      title: newNoteTitle,
      content: newNoteContent,
    };
    setNotes([...notes, newNote]);
    setNewNoteTitle('');
    setNewNoteContent('');
  };

  const handleDeleteNote = (id) => {
    setNotes(notes.filter((note) => note.id !== id));
  };

  const handleEditNote = (id) => {
    const noteToEdit = notes.find((note) => note.id === id);
    if (noteToEdit) {
      setEditingNoteId(id);
      setEditNoteTitle(noteToEdit.title);
      setEditNoteContent(noteToEdit.content);
    }
  };

  const handleEditTitleChange = (e) => {
    setEditNoteTitle(e.target.value);
  };

  const handleEditContentChange = (e) => {
    setEditNoteContent(e.target.value);
  };

  const handleSaveEdit = () => {
    if (editNoteTitle.trim() === '' || editNoteContent.trim() === '') {
      alert('Please fill in both title and content.');
      return;
    }
    setNotes(
      notes.map((note) =>
        note.id === editingNoteId ? { ...note, title: editNoteTitle, content: editNoteContent } : note
      )
    );
    setEditingNoteId(null);
    setEditNoteTitle('');
    setEditNoteContent('');
  };

  return (
    <div>
      <h1>My Notes</h1>
      <div>
        
        
        <button>Add Note</button>
      </div>
      {notes.map((note) => (
        <div>
          {editingNoteId === note.id ? (
            <div>
              
              
              <button>Save</button>
              <button> setEditingNoteId(null)}>Cancel</button>
            </div>
          ) : (
            <div>
              <h3>{note.title}</h3>
              <p>{note.content}</p>
              <button> handleEditNote(note.id)}>Edit</button>
              <button> handleDeleteNote(note.id)}>Delete</button>
            </div>
          )}
        </div>
      ))}
    </div>
  );
};

export default Home;

Key changes include:

  • We added state variables `editingNoteId`, `editNoteTitle`, and `editNoteContent`.
  • `handleEditNote` now finds the note to edit and sets the state for editing.
  • We introduced `handleEditTitleChange`, `handleEditContentChange` and `handleSaveEdit` to handle changes in the edit form.
  • Inside the `map` function, we conditionally render the edit form or the note display based on `editingNoteId`.

Implementing Data Persistence (Local Storage)

Currently, our notes are lost when the page is refreshed. To make the application more useful, we’ll use local storage to persist the notes. Modify `pages/index.js` again:

// pages/index.js
import React, { useState, useEffect } from 'react';
import Note from '../components/Note';

const Home = () => {
  const [notes, setNotes] = useState([]);
  const [newNoteTitle, setNewNoteTitle] = useState('');
  const [newNoteContent, setNewNoteContent] = useState('');
  const [editingNoteId, setEditingNoteId] = useState(null);
  const [editNoteTitle, setEditNoteTitle] = useState('');
  const [editNoteContent, setEditNoteContent] = useState('');

  useEffect(() => {
    // Load notes from local storage on component mount
    if (typeof window !== 'undefined') {
      const storedNotes = localStorage.getItem('notes');
      if (storedNotes) {
        setNotes(JSON.parse(storedNotes));
      }
    }
  }, []);

  useEffect(() => {
    // Save notes to local storage whenever the notes state changes
    if (typeof window !== 'undefined') {
      localStorage.setItem('notes', JSON.stringify(notes));
    }
  }, [notes]);

  const handleTitleChange = (e) => {
    setNewNoteTitle(e.target.value);
  };

  const handleContentChange = (e) => {
    setNewNoteContent(e.target.value);
  };

  const handleAddNote = () => {
    if (newNoteTitle.trim() === '' || newNoteContent.trim() === '') {
      alert('Please fill in both title and content.');
      return;
    }

    const newNote = {
      id: Date.now(), // Simple unique ID
      title: newNoteTitle,
      content: newNoteContent,
    };
    setNotes([...notes, newNote]);
    setNewNoteTitle('');
    setNewNoteContent('');
  };

  const handleDeleteNote = (id) => {
    setNotes(notes.filter((note) => note.id !== id));
  };

  const handleEditNote = (id) => {
    const noteToEdit = notes.find((note) => note.id === id);
    if (noteToEdit) {
      setEditingNoteId(id);
      setEditNoteTitle(noteToEdit.title);
      setEditNoteContent(noteToEdit.content);
    }
  };

  const handleEditTitleChange = (e) => {
    setEditNoteTitle(e.target.value);
  };

  const handleEditContentChange = (e) => {
    setEditNoteContent(e.target.value);
  };

  const handleSaveEdit = () => {
    if (editNoteTitle.trim() === '' || editNoteContent.trim() === '') {
      alert('Please fill in both title and content.');
      return;
    }
    setNotes(
      notes.map((note) =>
        note.id === editingNoteId ? { ...note, title: editNoteTitle, content: editNoteContent } : note
      )
    );
    setEditingNoteId(null);
    setEditNoteTitle('');
    setEditNoteContent('');
  };

  return (
    <div>
      <h1>My Notes</h1>
      <div>
        
        
        <button>Add Note</button>
      </div>
      {notes.map((note) => (
        <div>
          {editingNoteId === note.id ? (
            <div>
              
              
              <button>Save</button>
              <button> setEditingNoteId(null)}>Cancel</button>
            </div>
          ) : (
            <div>
              <h3>{note.title}</h3>
              <p>{note.content}</p>
              <button> handleEditNote(note.id)}>Edit</button>
              <button> handleDeleteNote(note.id)}>Delete</button>
            </div>
          )}
        </div>
      ))}
    </div>
  );
};

export default Home;

The key changes are:

  • We import the `useEffect` hook.
  • Loading Notes: Inside a `useEffect` hook with an empty dependency array (`[]`), we load notes from local storage when the component mounts. We check if `window` is defined to avoid errors during server-side rendering (Next.js handles both client and server-side). If notes are found in local storage, we parse them from JSON and update the `notes` state.
  • Saving Notes: Another `useEffect` hook, with `notes` as a dependency, saves the `notes` state to local storage whenever the `notes` array changes. We stringify the `notes` array to store it as a JSON string.

Common Mistakes and How to Fix Them

Here are some common mistakes beginners make when building Next.js applications and how to avoid them:

  • Incorrect File Paths: Ensure your file paths are accurate, especially when importing components. Double-check the relative paths in your `import` statements.
  • Missing Dependencies: If you encounter errors related to missing modules, install them using npm or yarn. For example, if you’re using a library, run `npm install [library-name]`.
  • Server-Side Rendering (SSR) vs. Client-Side Rendering (CSR) Confusion: Next.js uses SSR by default, but you might need to handle client-side specific operations carefully. Be mindful of the `window` object, which is only available in the browser. Use conditional rendering or checks like `typeof window !== ‘undefined’` to prevent errors in SSR.
  • Incorrect State Updates: When updating state using the `useState` hook, always create a new array or object instead of directly modifying the existing state. Use the spread operator (`…`) to create copies of arrays and objects.
  • Forgetting to Handle Events: Make sure you correctly wire up event handlers (e.g., `onClick`, `onChange`) to your components and pass the necessary props and event objects.
  • Ignoring Error Messages: Read the error messages in your console carefully. They often provide valuable clues about what’s going wrong.

Key Takeaways

In this tutorial, we’ve covered the essentials of building a simple Notes application with Next.js. We’ve learned how to set up a Next.js project, create components, manage state, handle user input, implement CRUD operations, and persist data using local storage. This project provides a solid foundation for understanding the core concepts of Next.js and React. You can expand upon this application by adding features such as:

  • More Advanced Styling: Using CSS modules, styled-components, or a CSS framework like Tailwind CSS.
  • Authentication: Allowing users to sign in and save their notes securely.
  • Database Integration: Storing notes in a database (e.g., MongoDB, PostgreSQL) for more robust data management.
  • Rich Text Editor: Using a library like Draft.js or React Quill to create rich text notes.
  • Search and Filtering: Adding search and filtering capabilities to find notes quickly.
  • Deployment: Deploying your Next.js application to platforms like Vercel or Netlify.

Remember that the key to mastering any framework is practice. Build this application, experiment with it, and try adding new features. The more you build, the more confident you’ll become. By starting small and gradually adding complexity, you’ll develop a strong understanding of Next.js and its capabilities. Web development is a journey of continuous learning, so keep exploring, keep building, and keep pushing your boundaries.