Build a Simple Next.js Interactive Note-Taking App

Written by

in

In today’s fast-paced digital world, taking and organizing notes efficiently is crucial for productivity. Whether you’re a student, professional, or simply someone who likes to jot down ideas, a reliable note-taking app can be a game-changer. This tutorial will guide you through building a simple, yet functional, note-taking application using Next.js, a powerful React framework, perfect for beginners and intermediate developers looking to enhance their web development skills.

Why Build a Note-Taking App?

Creating a note-taking app offers several benefits:

  • Practical Application: You’ll learn fundamental concepts of web development, including state management, user interface (UI) design, and data persistence.
  • Personalized Learning: Building a project from scratch allows you to customize features and tailor the app to your specific needs.
  • Portfolio Piece: A well-executed note-taking app can be a great addition to your portfolio, showcasing your skills to potential employers or clients.
  • Problem Solving: You’ll practice problem-solving skills, debugging, and understanding how different components interact.

Prerequisites

Before we begin, ensure you have the following:

  • Node.js and npm (or yarn): You’ll need these to manage project dependencies and run the development server.
  • A Code Editor: Visual Studio Code, Sublime Text, or any editor of your choice.
  • Basic HTML, CSS, and JavaScript knowledge: Familiarity with these languages will be helpful.

Setting Up Your Next.js Project

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

npx create-next-app my-note-app

Replace my-note-app with your preferred project name. This command will set up a basic Next.js application with all the necessary files and dependencies. Once the installation is complete, navigate to your project directory:

cd my-note-app

Now, start the development server:

npm run dev

Open your browser and go to http://localhost:3000 to see the default Next.js welcome page. If you see the page, congratulations! Your project is set up correctly.

Designing the User Interface (UI)

The UI will consist of the following components:

  • Note Input: A text area for entering the note content.
  • Add Note Button: A button to save the note.
  • Note List: A display of all the saved notes.
  • Individual Note Items: Each note will have a title and content, and maybe a delete button.

Let’s modify the pages/index.js file to create our basic UI. Replace the existing code with the following:

import { useState } from 'react';

export default function Home() {
  const [notes, setNotes] = useState([]);
  const [newNote, setNewNote] = useState('');

  const handleNoteChange = (event) => {
    setNewNote(event.target.value);
  };

  const handleAddNote = () => {
    if (newNote.trim() !== '') {
      setNotes([...notes, { id: Date.now(), text: newNote }]);
      setNewNote('');
    }
  };

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

  return (
    <div className="container">
      <h1>My Notes</h1>
      <div className="input-area">
        <textarea
          value={newNote}
          onChange={handleNoteChange}
          placeholder="Write your note here..."
        />
        <button onClick={handleAddNote}>Add Note</button>
      </div>
      <div className="note-list">
        {notes.map((note) => (
          <div key={note.id} className="note-item">
            <p>{note.text}</p>
            <button onClick={() => handleDeleteNote(note.id)}>Delete</button>
          </div>
        ))}
      </div>
    </div>
  );
}

In this code:

  • We import the useState hook from React to manage the state of our notes and the input field.
  • We initialize two state variables: notes (an array to store our notes) and newNote (a string to hold the current input).
  • handleNoteChange updates the newNote state as the user types in the text area.
  • handleAddNote adds a new note to the notes array when the button is clicked. It also clears the input field.
  • handleDeleteNote removes a note from the notes array based on its ID.
  • The UI consists of a text area, an “Add Note” button, and a list of notes displayed using the map function.

To make the app look better, let’s add some basic CSS. Create a file called styles/Home.module.css in your project directory (if it doesn’t already exist). Add the following CSS:

.container {
  width: 80%;
  margin: 0 auto;
  padding: 20px;
  font-family: sans-serif;
}

.input-area {
  margin-bottom: 20px;
}

textarea {
  width: 100%;
  padding: 10px;
  margin-bottom: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
  resize: vertical;
}

button {
  background-color: #4CAF50;
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

button:hover {
  background-color: #3e8e41;
}

.note-item {
  padding: 10px;
  margin-bottom: 10px;
  border: 1px solid #eee;
  border-radius: 4px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

Import this CSS file into your pages/index.js file by adding this line at the top of the file:

import styles from '../styles/Home.module.css';

And then, apply these styles to your components in the main return statement, for instance, by adding className={styles.container} to the main div element, and so on. Your pages/index.js should now look like this:

import { useState } from 'react';
import styles from '../styles/Home.module.css';

export default function Home() {
  const [notes, setNotes] = useState([]);
  const [newNote, setNewNote] = useState('');

  const handleNoteChange = (event) => {
    setNewNote(event.target.value);
  };

  const handleAddNote = () => {
    if (newNote.trim() !== '') {
      setNotes([...notes, { id: Date.now(), text: newNote }]);
      setNewNote('');
    }
  };

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

  return (
    <div className={styles.container}>
      <h1>My Notes</h1>
      <div className={styles.inputArea}>
        <textarea
          value={newNote}
          onChange={handleNoteChange}
          placeholder="Write your note here..."
        />
        <button onClick={handleAddNote}>Add Note</button>
      </div>
      <div className={styles.noteList}>
        {notes.map((note) => (
          <div key={note.id} className={styles.noteItem}>
            <p>{note.text}</p>
            <button onClick={() => handleDeleteNote(note.id)}>Delete</button>
          </div>
        ))}
      </div>
    </div>
  );
}

Save both files and refresh your browser. You should now see a basic note-taking interface with a text area, an “Add Note” button, and a list to display your notes. You can type in the text area, click “Add Note”, and your note will appear below. Clicking “Delete” removes the note.

Adding Features: Local Storage

Currently, our notes disappear when you refresh the page. To make them persist, we’ll use local storage. Local storage allows us to save data in the user’s browser, so it’s available even after the page is refreshed or the browser is closed and reopened.

Modify your pages/index.js file to include the following changes:

import { useState, useEffect } from 'react';
import styles from '../styles/Home.module.css';

export default function Home() {
  const [notes, setNotes] = useState([]);
  const [newNote, setNewNote] = useState('');

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

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

  const handleNoteChange = (event) => {
    setNewNote(event.target.value);
  };

  const handleAddNote = () => {
    if (newNote.trim() !== '') {
      setNotes([...notes, { id: Date.now(), text: newNote }]);
      setNewNote('');
    }
  };

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

  return (
    <div className={styles.container}>
      <h1>My Notes</h1>
      <div className={styles.inputArea}>
        <textarea
          value={newNote}
          onChange={handleNoteChange}
          placeholder="Write your note here..."
        />
        <button onClick={handleAddNote}>Add Note</button>
      </div>
      <div className={styles.noteList}>
        {notes.map((note) => (
          <div key={note.id} className={styles.noteItem}>
            <p>{note.text}</p>
            <button onClick={() => handleDeleteNote(note.id)}>Delete</button>
          </div>
        ))}
      </div>
    </div>
  );
}

Here’s what’s new:

  • We import the useEffect hook from React.
  • We use useEffect to load notes from local storage when the component mounts (when the page first loads). We use localStorage.getItem('notes') to retrieve the stored data and parse it using JSON.parse(). We check if typeof window !== 'undefined' to ensure this code runs only on the client-side (in the browser), as localStorage is not available during server-side rendering.
  • Another useEffect hook is used to save the notes to local storage whenever the notes state changes. We use localStorage.setItem('notes', JSON.stringify(notes)) to store the data as a string, using JSON.stringify() to convert the array to a string. This hook also checks if typeof window !== 'undefined' for the same reason. The [notes] in the dependency array means this effect runs whenever the notes state changes.

Now, when you refresh the page, your notes should persist!

Enhancing the App: Note Titles and Editing

Let’s add more features to make our note-taking app more useful. We’ll add titles to our notes and provide an edit functionality.

First, modify the state of notes to include a title. Also, add the title to the input form.

import { useState, useEffect } from 'react';
import styles from '../styles/Home.module.css';

export default function Home() {
  const [notes, setNotes] = useState([]);
  const [newNote, setNewNote] = useState('');
  const [newNoteTitle, setNewNoteTitle] = useState(''); // New state for note title
  const [editingNoteId, setEditingNoteId] = useState(null); // To track which note is being edited

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

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

  const handleNoteTitleChange = (event) => {
    setNewNoteTitle(event.target.value);
  };

  const handleNoteChange = (event) => {
    setNewNote(event.target.value);
  };

  const handleAddNote = () => {
    if (newNote.trim() !== '' && newNoteTitle.trim() !== '') {
      setNotes([
        ...notes,
        {
          id: Date.now(),
          title: newNoteTitle,
          text: newNote,
        },
      ]);
      setNewNote('');
      setNewNoteTitle(''); // Clear title input
    }
  };

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

  const handleEditNote = (id) => {
    setEditingNoteId(id);
    const noteToEdit = notes.find((note) => note.id === id);
    if (noteToEdit) {
      setNewNote(noteToEdit.text);
      setNewNoteTitle(noteToEdit.title);
    }
  };

  const handleUpdateNote = () => {
    if (newNote.trim() !== '' && newNoteTitle.trim() !== '') {
      setNotes(
        notes.map((note) =>
          note.id === editingNoteId
            ? { ...note, title: newNoteTitle, text: newNote } // Update the note
            : note
        )
      );
      setEditingNoteId(null);
      setNewNote('');
      setNewNoteTitle('');
    }
  };

  return (
    <div className={styles.container}>
      <h1>My Notes</h1>
      <div className={styles.inputArea}>
        <input
          type="text"
          value={newNoteTitle}
          onChange={handleNoteTitleChange}
          placeholder="Note Title"
        />
        <textarea
          value={newNote}
          onChange={handleNoteChange}
          placeholder="Write your note here..."
        />
        {editingNoteId ? (
          <button onClick={handleUpdateNote}>Update Note</button>
        ) : (
          <button onClick={handleAddNote}>Add Note</button>
        )}
      </div>
      <div className={styles.noteList}>
        {notes.map((note) => (
          <div key={note.id} className={styles.noteItem}>
            <h3>{note.title}</h3> {/* Display the title */}
            <p>{note.text}</p>
            <button onClick={() => handleEditNote(note.id)}>Edit</button> {/* Add an edit button */}
            <button onClick={() => handleDeleteNote(note.id)}>Delete</button>
          </div>
        ))}
      </div>
    </div>
  );
}

Changes in this version:

  • We added a new state variable, newNoteTitle, to manage the title input.
  • A new state variable, editingNoteId, tracks the ID of the note being edited, which will be null when no note is being edited.
  • We added handleNoteTitleChange to update the title input.
  • In handleAddNote, we now include the title when adding a new note.
  • We added handleEditNote to set the editingNoteId and pre-populate the input fields with the note’s content and title.
  • We added handleUpdateNote to update the note in the array.
  • The UI now includes an input field for the title and displays the title in the note list.
  • We’ve added an Edit button for each note and conditionally render an Update Note button or an Add Note button based on whether we are editing a note.

To style the title and edit button, add the following to your styles/Home.module.css file:

input[type="text"] {
  width: 100%;
  padding: 10px;
  margin-bottom: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

With these changes, you can now add titles to your notes, edit them, and delete them. The app is much more functional!

Common Mistakes and Troubleshooting

Here are some common mistakes and how to fix them:

  • Notes Not Persisting: Double-check that you’ve correctly implemented the useEffect hooks for loading and saving notes to local storage. Make sure the dependencies arrays ([] and [notes]) are correct. Also, ensure the local storage functions are wrapped in a conditional check for the client-side environment (typeof window !== 'undefined').
  • Data Not Displaying: If your notes aren’t displaying, verify that you are correctly mapping through the notes array and that the data structure in local storage matches the expected format. Use the browser’s developer tools (Console) to inspect the notes array and local storage values to debug.
  • Incorrect State Updates: Ensure that you are using the correct state update functions (setNotes, setNewNote, setNewNoteTitle) and that the logic for adding, editing, and deleting notes is accurate.
  • CSS Issues: Double-check your CSS file paths and class names to ensure the styles are being applied correctly. Use the browser’s developer tools (Elements) to inspect the HTML and CSS and identify any styling issues.

Optimizing and Expanding the App

To optimize and expand your app further, consider the following:

  • Error Handling: Implement error handling for local storage operations. What happens if local storage is full or unavailable?
  • Rich Text Editor: Integrate a rich text editor (e.g., Draft.js, Quill.js) for more advanced formatting options.
  • Search Functionality: Add a search feature to easily find notes.
  • Note Categories/Tags: Implement categories or tags to organize your notes.
  • User Authentication: For a real-world application, consider adding user authentication to allow multiple users to save their notes securely.
  • Deployment: Deploy your app to a platform like Vercel or Netlify to share it with the world.

Summary / Key Takeaways

In this tutorial, we’ve built a simple note-taking app using Next.js. We’ve covered the basics of UI design, state management with React hooks (useState and useEffect), and data persistence using local storage. This project provides a solid foundation for understanding the core concepts of web development and building interactive web applications with Next.js.

FAQ

Here are some frequently asked questions:

  1. Can I use a database instead of local storage? Yes, for more complex applications, you can use a database (e.g., MongoDB, PostgreSQL) to store your notes. This will allow for features like user accounts, data backup, and scalability.
  2. How do I deploy my Next.js app? You can deploy your Next.js app to platforms like Vercel, Netlify, or AWS. These platforms provide easy deployment workflows and handle the server-side rendering and build processes.
  3. What are some other Next.js features I can use? Next.js offers many features, including server-side rendering (SSR), static site generation (SSG), API routes, and image optimization. Explore these features to enhance your app further.
  4. Can I use a different styling library? Yes, you can use any styling library you prefer, such as Styled Components, Material UI, or Tailwind CSS.

Building this note-taking app is just the beginning. The world of web development is vast and exciting, and with the skills you’ve gained, you can now start exploring more advanced projects. Remember to practice consistently, experiment with different features, and embrace the learning process. The ability to create functional and user-friendly web applications is a valuable skill in today’s digital landscape. Keep coding, keep learning, and keep building!