In today’s digital world, we’re constantly bombarded with information. Finding and saving the articles, websites, and resources that truly resonate with us can be a real challenge. That’s where a bookmarking app comes in handy. Imagine having a personalized library of your favorite web content, easily accessible and organized. In this tutorial, we’ll build a simple, yet functional, bookmarking app using ReactJS. This project is perfect for beginner to intermediate developers looking to solidify their React skills and understand how to manage state, handle user input, and render dynamic content.
Why Build a Bookmarking App?
Building a bookmarking app offers several benefits:
- Practical Application: It’s a real-world project that solves a common problem.
- Skill Development: You’ll learn core React concepts like state management, component composition, and event handling.
- Portfolio Piece: It’s a great project to showcase your React skills to potential employers.
- Personal Use: You’ll have a useful tool for organizing your online resources.
This project is designed to be a stepping stone. As you progress, you can add more features, such as categories, search functionality, and the ability to save bookmarks to a database.
Prerequisites
Before we begin, make sure you have the following:
- Node.js and npm (or yarn) installed: You’ll need these to manage project dependencies.
- A code editor: Visual Studio Code, Sublime Text, or any editor you prefer.
- Basic understanding of HTML, CSS, and JavaScript: Familiarity with these languages is essential.
Setting Up the Project
Let’s get started by creating a new React app. Open your terminal and run the following command:
npx create-react-app bookmarking-app
This command sets up a new React project with all the necessary files and dependencies. Once the project is created, navigate into the project directory:
cd bookmarking-app
Now, let’s clean up the boilerplate code. Open the `src/App.js` file and replace its contents with the following:
import React, { useState } from 'react';
import './App.css';
function App() {
return (
<div className="app">
<h1>My Bookmarks</h1>
</div>
);
}
export default App;
Also, clear the contents of `src/App.css` and `src/index.css` to remove any default styling that might interfere with our app. Finally, run the app using:
npm start
This will open the app in your browser, typically at `http://localhost:3000`. You should see the “My Bookmarks” heading.
Building the Bookmark Form
The first step is to create a form where users can add new bookmarks. We’ll need input fields for the bookmark’s title and URL. Let’s create a new component called `BookmarkForm.js` in a new `components` folder inside `src`:
mkdir src/components
touch src/components/BookmarkForm.js
Add the following code to `BookmarkForm.js`:
import React, { useState } from 'react';
function BookmarkForm({ onAddBookmark }) {
const [title, setTitle] = useState('');
const [url, setUrl] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (title.trim() && url.trim()) {
onAddBookmark({ title, url });
setTitle('');
setUrl('');
}
};
return (
<form onSubmit={handleSubmit} className="bookmark-form">
<label htmlFor="title">Title:</label>
<input
type="text"
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<label htmlFor="url">URL:</label>
<input
type="url"
id="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
<button type="submit">Add Bookmark</button>
</form>
);
}
export default BookmarkForm;
Let’s break down the code:
- `useState` Hooks: We use `useState` to manage the `title` and `url` input values.
- `handleSubmit` Function: This function is called when the form is submitted. It prevents the default form submission behavior (page reload), calls the `onAddBookmark` prop (which we’ll define later), and clears the input fields.
- `onAddBookmark` Prop: This prop will be a function passed from the parent component (`App.js`) to handle adding the bookmark to the bookmark list.
- Form Structure: The form includes input fields for the title and URL, along with labels and a submit button.
Now, let’s import and use this component in `App.js`:
import React, { useState } from 'react';
import './App.css';
import BookmarkForm from './components/BookmarkForm';
function App() {
const [bookmarks, setBookmarks] = useState([]);
const handleAddBookmark = (newBookmark) => {
setBookmarks([...bookmarks, newBookmark]);
};
return (
<div className="app">
<h1>My Bookmarks</h1>
<BookmarkForm onAddBookmark={handleAddBookmark} />
</div>
);
}
export default App;
Here’s what changed:
- Imported `BookmarkForm`
- Added `bookmarks` state: This will store the list of bookmarks.
- `handleAddBookmark` function: This function is passed to `BookmarkForm` via the `onAddBookmark` prop. It receives the new bookmark object and updates the `bookmarks` state using the spread operator (`…`) to create a new array with the existing bookmarks and the new bookmark.
- Rendered `BookmarkForm`: We pass the `handleAddBookmark` function as a prop to the `BookmarkForm` component.
Let’s add some basic styling to make the form look better. Add the following CSS to `src/App.css`:
.app {
max-width: 600px;
margin: 20px auto;
padding: 20px;
border: 1px solid #ccc;
border-radius: 5px;
}
.bookmark-form {
display: flex;
flex-direction: column;
margin-bottom: 20px;
}
.bookmark-form label {
margin-bottom: 5px;
font-weight: bold;
}
.bookmark-form input {
padding: 8px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
.bookmark-form button {
padding: 10px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
Now, when you enter a title and URL and click “Add Bookmark,” the bookmark data will be stored in the `bookmarks` state, but we’re not yet displaying them. Let’s move on to rendering the bookmarks.
Displaying Bookmarks
We’ll create a component called `BookmarkList.js` to display the list of bookmarks. Create this file in the `components` folder:
touch src/components/BookmarkList.js
Add the following code to `BookmarkList.js`:
import React from 'react';
function BookmarkList({ bookmarks }) {
return (
<ul className="bookmark-list">
{bookmarks.map((bookmark, index) => (
<li key={index} className="bookmark-item">
<a href={bookmark.url} target="_blank" rel="noopener noreferrer">
{bookmark.title}
</a>
</li>
))}
</ul>
);
}
export default BookmarkList;
Let’s break down the code:
- `bookmarks` Prop: This component receives an array of `bookmarks` as a prop.
- `map` Method: The `map` method iterates over the `bookmarks` array and renders a `li` element for each bookmark.
- `key` Prop: Each `li` element has a unique `key` prop (the index) to help React efficiently update the list.
- `a` Tag: The `a` tag creates a link to the bookmark’s URL. The `target=”_blank”` attribute opens the link in a new tab, and `rel=”noopener noreferrer”` is a security best practice.
Now, let’s import and use the `BookmarkList` component in `App.js`:
import React, { useState } from 'react';
import './App.css';
import BookmarkForm from './components/BookmarkForm';
import BookmarkList from './components/BookmarkList';
function App() {
const [bookmarks, setBookmarks] = useState([]);
const handleAddBookmark = (newBookmark) => {
setBookmarks([...bookmarks, newBookmark]);
};
return (
<div className="app">
<h1>My Bookmarks</h1>
<BookmarkForm onAddBookmark={handleAddBookmark} />
<BookmarkList bookmarks={bookmarks} />
</div>
);
}
export default App;
Here’s what changed:
- Imported `BookmarkList`
- Rendered `BookmarkList`: We pass the `bookmarks` state as a prop to the `BookmarkList` component.
Add the following CSS to `src/App.css` to style the bookmark list:
.bookmark-list {
list-style: none;
padding: 0;
}
.bookmark-item {
padding: 10px;
border-bottom: 1px solid #eee;
}
.bookmark-item:last-child {
border-bottom: none;
}
.bookmark-item a {
text-decoration: none;
color: #333;
font-weight: bold;
}
.bookmark-item a:hover {
color: #007bff;
}
Now, when you add a bookmark, it should appear in the list below the form. Congratulations, you’ve built a basic bookmarking app!
Adding Bookmark Deletion Functionality
Next, let’s add the ability to delete bookmarks. We’ll add a delete button next to each bookmark in the `BookmarkList` component.
First, modify the `BookmarkList.js` file to include a delete button and an `onDelete` prop:
import React from 'react';
function BookmarkList({ bookmarks, onDelete }) {
return (
<ul className="bookmark-list">
{bookmarks.map((bookmark, index) => (
<li key={index} className="bookmark-item">
<a href={bookmark.url} target="_blank" rel="noopener noreferrer">
{bookmark.title}
</a>
<button onClick={() => onDelete(index)} className="delete-button">Delete</button>
</li>
))}
</ul>
);
}
export default BookmarkList;
Changes:
- Added the `onDelete` prop to the function signature.
- Added a delete button next to each bookmark. The `onClick` event handler calls the `onDelete` function, passing the bookmark’s index as an argument.
Now, update `App.js` to pass the `onDelete` function to `BookmarkList` and implement the deletion logic:
import React, { useState } from 'react';
import './App.css';
import BookmarkForm from './components/BookmarkForm';
import BookmarkList from './components/BookmarkList';
function App() {
const [bookmarks, setBookmarks] = useState([]);
const handleAddBookmark = (newBookmark) => {
setBookmarks([...bookmarks, newBookmark]);
};
const handleDeleteBookmark = (index) => {
const newBookmarks = [...bookmarks];
newBookmarks.splice(index, 1);
setBookmarks(newBookmarks);
};
return (
<div className="app">
<h1>My Bookmarks</h1>
<BookmarkForm onAddBookmark={handleAddBookmark} />
<BookmarkList bookmarks={bookmarks} onDelete={handleDeleteBookmark} />
</div>
);
}
export default App;
Changes:
- Added the `handleDeleteBookmark` function. This function takes the index of the bookmark to delete, creates a copy of the `bookmarks` array, uses `splice` to remove the bookmark at the specified index, and updates the state.
- Passed the `handleDeleteBookmark` function as the `onDelete` prop to `BookmarkList`.
Add the following CSS to `src/App.css` to style the delete button:
.delete-button {
margin-left: 10px;
padding: 5px 10px;
background-color: #f44336;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.delete-button:hover {
background-color: #d32f2f;
}
Now, when you click the “Delete” button next to a bookmark, it will be removed from the list.
Adding Local Storage for Persistence
Currently, the bookmarks are only stored in the component’s state. If you refresh the page, the bookmarks disappear. To persist the bookmarks, we can use local storage.
Modify `App.js` to use local storage:
import React, { useState, useEffect } from 'react';
import './App.css';
import BookmarkForm from './components/BookmarkForm';
import BookmarkList from './components/BookmarkList';
function App() {
const [bookmarks, setBookmarks] = useState(() => {
// Load bookmarks from local storage on component mount
const savedBookmarks = localStorage.getItem('bookmarks');
return savedBookmarks ? JSON.parse(savedBookmarks) : [];
});
useEffect(() => {
// Save bookmarks to local storage whenever the bookmarks state changes
localStorage.setItem('bookmarks', JSON.stringify(bookmarks));
}, [bookmarks]);
const handleAddBookmark = (newBookmark) => {
setBookmarks([...bookmarks, newBookmark]);
};
const handleDeleteBookmark = (index) => {
const newBookmarks = [...bookmarks];
newBookmarks.splice(index, 1);
setBookmarks(newBookmarks);
};
return (
<div className="app">
<h1>My Bookmarks</h1>
<BookmarkForm onAddBookmark={handleAddBookmark} />
<BookmarkList bookmarks={bookmarks} onDelete={handleDeleteBookmark} />
</div>
);
}
export default App;
Changes:
- Imported `useEffect`
- Initial State with Local Storage: The `useState` hook now initializes the `bookmarks` state by checking local storage for saved bookmarks. If bookmarks exist in local storage, they are parsed from JSON and used as the initial state. If not, it defaults to an empty array.
- `useEffect` Hook: The `useEffect` hook is used to save the bookmarks to local storage whenever the `bookmarks` state changes. The hook takes two arguments: a callback function (which saves the bookmarks) and a dependency array (`[bookmarks]`). The dependency array ensures that the effect runs only when the `bookmarks` state changes.
- `JSON.stringify()` and `JSON.parse()`: `JSON.stringify()` is used to convert the `bookmarks` array to a JSON string before saving it to local storage, and `JSON.parse()` is used to convert the JSON string back into a JavaScript array when loading from local storage.
Now, your bookmarks will be saved to your browser’s local storage and will persist even after you refresh the page or close and reopen the browser.
Adding Input Validation
To improve the user experience, let’s add some input validation to the bookmark form. We’ll ensure that the URL is a valid URL and provide feedback to the user if it’s not.
Modify the `BookmarkForm.js` file as follows:
import React, { useState } from 'react';
function BookmarkForm({ onAddBookmark }) {
const [title, setTitle] = useState('');
const [url, setUrl] = useState('');
const [error, setError] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (!title.trim() || !url.trim()) {
setError('Please fill in both title and URL.');
return;
}
if (!isValidUrl(url)) {
setError('Please enter a valid URL.');
return;
}
onAddBookmark({ title, url });
setTitle('');
setUrl('');
setError('');
};
const isValidUrl = (urlString) => {
try {
new URL(urlString);
return true;
} catch (error) {
return false;
}
};
return (
<form onSubmit={handleSubmit} className="bookmark-form">
<label htmlFor="title">Title:</label>
<input
type="text"
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<label htmlFor="url">URL:</label>
<input
type="url"
id="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
{error && <p className="error-message">{error}</p>}
<button type="submit">Add Bookmark</button>
</form>
);
}
export default BookmarkForm;
Changes:
- Added `error` State: A new `error` state is added to store any validation error messages.
- `handleSubmit` Function Updated: The `handleSubmit` function now checks for empty title or URL fields and displays an error message if either is missing. It also calls the `isValidUrl` function to check if the URL is valid. If the URL is invalid, an error message is displayed.
- `isValidUrl` Function: This function uses the `URL` constructor to check if the provided string is a valid URL. If the URL is invalid, it catches an error and returns `false`.
- Error Message Display: An error message is displayed below the input fields if the `error` state is not empty.
Add the following CSS to `src/App.css` to style the error message:
.error-message {
color: red;
margin-top: 5px;
}
Now, the form will display an error message if the user enters an invalid URL or leaves either the title or URL fields blank.
Adding a Search Feature
To make the app more user-friendly, let’s add a search feature to filter the bookmarks. This will allow users to quickly find specific bookmarks.
First, add a new state variable in `App.js` to store the search term:
import React, { useState, useEffect } from 'react';
import './App.css';
import BookmarkForm from './components/BookmarkForm';
import BookmarkList from './components/BookmarkList';
function App() {
const [bookmarks, setBookmarks] = useState(() => {
const savedBookmarks = localStorage.getItem('bookmarks');
return savedBookmarks ? JSON.parse(savedBookmarks) : [];
});
const [searchTerm, setSearchTerm] = useState('');
useEffect(() => {
localStorage.setItem('bookmarks', JSON.stringify(bookmarks));
}, [bookmarks]);
const handleAddBookmark = (newBookmark) => {
setBookmarks([...bookmarks, newBookmark]);
};
const handleDeleteBookmark = (index) => {
const newBookmarks = [...bookmarks];
newBookmarks.splice(index, 1);
setBookmarks(newBookmarks);
};
return (
<div className="app">
<h1>My Bookmarks</h1>
<BookmarkForm onAddBookmark={handleAddBookmark} />
<input
type="text"
placeholder="Search bookmarks..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<BookmarkList
bookmarks={bookmarks.filter((bookmark) =>
bookmark.title.toLowerCase().includes(searchTerm.toLowerCase())
)}
onDelete={handleDeleteBookmark}
/>
</div>
);
}
export default App;
Changes:
- Added `searchTerm` State: A new state variable, `searchTerm`, is added to store the user’s search input.
- Search Input: An input field is added above the `BookmarkList` for the user to enter their search query. The `onChange` event handler updates the `searchTerm` state.
- Filtering Bookmarks: The `BookmarkList` component now receives a filtered list of bookmarks. The `bookmarks.filter()` method is used to filter the original `bookmarks` array based on the `searchTerm`. The `toLowerCase()` method is used to make the search case-insensitive, and `includes()` checks if the bookmark title contains the search term.
Now, when the user types in the search input, the bookmark list will be filtered to show only bookmarks whose titles match the search term.
Common Mistakes and How to Fix Them
Here are some common mistakes beginners make when building React apps, along with solutions:
- Incorrect State Updates: Failing to update the state correctly can lead to unexpected behavior. Always use the `set…` functions provided by the `useState` hook to update state. When updating state based on the previous state, use the functional form of `set…` (e.g., `setBookmarks(prevBookmarks => […prevBookmarks, newBookmark])`).
- Forgetting the `key` Prop: When rendering lists of elements using `map`, always provide a unique `key` prop to each element. This helps React efficiently update the list. The index is usually fine unless the list can be reordered or items can be inserted/removed in the middle. In those cases, use a unique ID for each item.
- Not Handling Events Correctly: Make sure to pass event handlers correctly and prevent default browser behavior when necessary (e.g., form submissions).
- Incorrect Prop Drilling: Passing props down multiple levels of components can become cumbersome. Consider using Context or a state management library like Redux or Zustand for more complex applications.
- Ignoring the Console: The browser’s console is your friend! It often provides helpful error messages and warnings. Pay attention to the console while developing.
Key Takeaways
This tutorial covered the following key concepts:
- Component Creation: Building reusable components is the foundation of React.
- State Management: Using `useState` to manage component data and trigger re-renders.
- Event Handling: Responding to user interactions, such as form submissions and button clicks.
- Rendering Lists: Using the `map` method to dynamically render lists of data.
- Prop Passing: Passing data and functions between components.
- Local Storage: Persisting data across browser sessions.
- Input Validation: Ensuring data integrity and providing a better user experience.
- Search Functionality: Filtering data based on user input.
FAQ
Here are some frequently asked questions:
- Can I add categories to my bookmarks? Yes, you can extend this app to include categories. You would need to add a category input field to the form, store the category with each bookmark, and modify the `BookmarkList` component to display and filter by category.
- How can I deploy this app? You can deploy your React app to platforms like Netlify, Vercel, or GitHub Pages. These platforms provide free hosting and make it easy to deploy your app.
- How can I add more features? You can add features like:
- Editing bookmarks
- Importing/exporting bookmarks
- Adding tags to bookmarks
- Integrating with a backend to store bookmarks in a database
- What are some good resources for learning React?
- The official React documentation
- React.dev
- FreeCodeCamp.org
- Scrimba.com
- Udemy and Coursera courses
By completing this tutorial, you’ve gained a solid understanding of fundamental React concepts and built a practical application. The bookmarking app is a great starting point, and you can now expand on it by adding more features and functionality to enhance your skills and create a more robust tool. Keep experimenting, exploring new features, and building projects. With each project you undertake, you’ll deepen your understanding of React and become a more proficient developer. The journey of learning and building never truly ends; it’s a continuous cycle of discovery, implementation, and refinement, and the more you practice, the more confident and capable you’ll become. So, keep coding, keep building, and keep learning!
