Back to Articles

Building a Dynamic Book Search App with React: A Comprehensive Guide

Posted: 4 months agoยทLast Updated: 1 month ago
Blog Cover
Share on LinkedIn
Share on X
Share on Facebook
Share on WhatsApp
Share on Telegram
Share via Email
Copy Link

Welcome to this step-by-step guide on building a dynamic book search application using React! We'll also dive into some key concepts, best practices, and potential improvements for making the app more robust.

The Big Picture

This application allows users to search for books from the Open Library API, displaying results dynamically as they scroll. The app consists of two main parts:

  • App.js: The main component that handles user input and displays the book results.
  • useBookSearch.js: A custom hook that manages the data fetching logic.

Setting Up a React Project

  • Install Node.js: Make sure you have Node.js installed. You can download it from nodejs.org.
  • Create a React App: Use Create React App to set up the project. Open your terminal and run:
npx create-react-app book-search-app
cd book-search-app
  • Install Axios: We'll use Axios for making HTTP requests. Install it by running:
npm install axios
  • Start the Development Server: Start the React development server:
npm start

With the project set up, let's dive into the code.

The Heart of Our Application

Let's start by looking at the App.js file, which is the central piece of our application:

import React, { useState, useRef, useCallback } from "react";
import useBookSearch from "./useBookSearch";
import "./App.css"; 

export default function App() {
  const [query, setQuery] = useState("");
  const [pageNumber, setPageNumber] = useState(1);

  const { books, hasMore, loading, error } = useBookSearch(query, pageNumber);

  const observer = useRef();
  const lastBookElementRef = useCallback(
    (node) => {
      if (loading) return;
      if (observer.current) observer.current.disconnect();
      observer.current = new IntersectionObserver((entries) => {
        if (entries[0].isIntersecting && hasMore) {
          setPageNumber((prevPageNumber) => prevPageNumber + 1);
        }
      });
      if (node) observer.current.observe(node);
    },
    [loading, hasMore]
  );

  function handleSearch(e) {
    setQuery(e.target.value);
    setPageNumber(1);
  }

  return (
    <div className="app-container">
      <div className="search-container">
        <input
          className="search-input"
          placeholder="Search Books"
          type="search"
          value={query}
          onChange={handleSearch}
        />
      </div>

      <div className="books-list">
        {loading && <div className="loading-text">Loading...</div>}
        {error && <div className="error-text">Error loading books.</div>}
        {!loading && books.length === 0 && (
          <div className="no-results-text">
            {query
              ? "No results found for this search. Ensure your search is correct "
              : "Search results will be displayed here."}
          </div>
        )}
        {books.map((book, index) => (
          <div
            key={book}
            ref={index === books.length - 1 ? lastBookElementRef : null}
            className="book-item"
          >
            {book}
          </div>
        ))}
        {!loading && !hasMore && books.length > 0 && (
          <div className="no-more-results-text">No more results.</div>
        )}
      </div>

      {/* Footer */}
      <footer className="footer">
        <p>
          &copy;{" "}
          <a
            href="https://github.com/dennismbugua/Search-Books"
            target="_blank"
            rel="noopener noreferrer"
          >
            Source Code
          </a>
        </p>
      </footer>
    </div>
  );
}

Breaking Down App.js

  • State Management: We use the useState hook to manage the search query and the current page number.
  • Custom Hook: The useBookSearch custom hook is used to fetch book data based on the query and page number.
  • Intersection Observer: This is a nifty feature that allows us to implement infinite scrolling. The useRef and useCallback hooks are used to create and manage the observer.
  • Event Handling: The handleSearch function updates the search query and resets the page number when the user types in the search input.

This structure ensures that our application is responsive and efficient, providing a smooth user experience.

The Data Fetching Powerhouse

Next, let's dive into the useBookSearch.js file, which encapsulates the logic for fetching book data from the Open Library API:

import { useEffect, useState } from "react";
import axios from "axios";

export default function useBookSearch(query, pageNumber) {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);
  const [books, setBooks] = useState([]);
  const [hasMore, setHasMore] = useState(false);

  useEffect(() => {
    setBooks([]);
    setLoading(true);

    const timer = setTimeout(() => {
      fetchData();
    }, 0.005); 

    return () => clearTimeout(timer); // Clear the timer on component unmount or query change
  }, [query, pageNumber]);

  const fetchData = () => {
    setError(false);
    let cancel;
    axios({
      method: "GET",
      url: "http://openlibrary.org/search.json",
      params: { q: query, page: pageNumber },
      cancelToken: new axios.CancelToken((c) => (cancel = c)),
    })
      .then((res) => {
        setBooks((prevBooks) => {
          return [
            ...new Set([...prevBooks, ...res.data.docs.map((b) => b.title)]),
          ];
        });
        setHasMore(res.data.docs.length > 0);
        setLoading(false);
      })
      .catch((e) => {
        if (axios.isCancel(e)) return;
        setError(true);
      });
    return () => cancel();
  };

  return { loading, error, books, hasMore };
}

Breaking Down useBookSearch.js

  • State Management: This custom hook uses useState to manage loading, error, books, and whether there are more books to load.
  • Effect Hook: The useEffect hook triggers the data fetch whenever the query or page number changes.
  • Data Fetching: Axios is used to make a GET request to the Open Library API. The results are processed and stored in the state.
  • Debouncing: A debounce mechanism ensures that the fetch operation is not performed too frequently, which helps in optimizing performance and avoiding unnecessary API calls.

Improving the Project

While the current implementation works well, there are several ways to enhance and make the project more robust:

  1. Enhanced Error Handling: Provide more detailed error messages and retry options.
  2. Loading Spinners and Skeletons: Use loading spinners or skeleton screens to improve the user experience during data fetching.
  3. Pagination Controls: Add buttons or links to allow users to manually navigate through pages of results.
  4. Book Details: Display more information about each book, such as author, cover image, and publication date.
  5. Accessibility Improvements: Ensure that the app is fully accessible, following WAI-ARIA guidelines.
  6. Testing: Implement unit and integration tests using tools like Jest and React Testing Library to ensure code quality and reliability.

Real-World Applications

Such dynamic search integrations can be applied in various real-world scenarios:

  • E-commerce Websites: Implementing infinite scrolling for product listings.
  • News Portals: Dynamically loading more articles as users scroll down.
  • Social Media: Loading additional posts or comments without requiring page reloads.
  • Job Boards: Allowing users to search and browse job listings efficiently.

Conclusion

Building a dynamic book search app with React involves understanding and integrating several key concepts such as state management, custom hooks, and the Intersection Observer API. By following this guide, you should have a solid foundation to create and customize your own search applications.

Feel free to explore and expand upon this example. Happy coding! And don't forget to check out the source code on GitHub for more insights and improvements.

Share on LinkedIn
Share on X
Share on Facebook
Share on WhatsApp
Share on Telegram
Share via Email
Copy Link

Meet Dennis, a seasoned software engineer with 10 years of experience transforming ideas into digital reality. He has successfully guided countless projects from concept to deployment, bringing innovative solutions to life. With a passion for crafting exceptional software, Dennis has helped countless clients achieve their goals.

Click here to learn more

Popular Posts

No popular posts available.

Recommended For You

No related posts found.