Imagine you're browsing through thousands of books online, but instead of waiting for each page to load slowly, you see results instantly as you type. That's exactly what we built - a lightning-fast book search application that combines multiple book databases to give you the best results possible.
The Problem We Solved
- Slow searches: Traditional search takes too long
- Limited results: Single data sources don't have everything
- Poor mobile experience: Most book sites aren't mobile-friendly
- Accessibility issues: Not everyone can use keyboard and mouse easily
Solution
- Instant search: Results appear as you type (with smart delays)
- Multiple sources: Combines Google Books and Open Library
- Mobile-first: Works perfectly on phones and tablets
- Accessible: Works with screen readers and keyboard navigation
What We're Building
Final Product Features
✅ Real-time Search: Type and see results instantly
✅ Smart Caching: Remember searches to avoid repeated requests
✅ Virtual Scrolling: Handle thousands of results smoothly
✅ Multiple APIs: Combine Google Books + Open Library
✅ Responsive Design: Beautiful on all screen sizes
✅ Accessibility: Screen reader and keyboard friendly
✅ Error Recovery: Graceful handling when things go wrong
Live Demo Preview
Tech Stack & Why We Chose It
Frontend Technologies
React 18 - The Foundation
// Why React?
const benefits = {
componentReuse: "Write once, use everywhere",
virtualDOM: "Lightning fast updates",
ecosystem: "Huge community and libraries",
jobMarket: "High demand skill"
};For Non-Technical Readers: React is like building with LEGO blocks - you create small, reusable pieces that snap together to build complex applications.
Tailwind CSS - The Styling
1/* Traditional CSS */
2.search-box {
3 width: 100%;
4 padding: 12px;
5 border: 1px solid #ccc;
6 border-radius: 8px;
7}
8
9/* Tailwind CSS - Much faster! */
10<input className="w-full p-3 border rounded-lg" />Why Tailwind: Instead of writing custom CSS, we use pre-built classes. It's like having a huge toolbox where every tool is labeled and ready to use.
Axios - API Communication
// Axios makes talking to servers simple
const response = await axios.get('https://api.books.com/search?q=harry+potter');For Non-Technical Readers: Axios is like a translator that helps our app talk to book databases on the internet.
Project Setup
Step 1: Initialize the Project
# Create a new React app
npx create-react-app search-books
cd search-books
# Install additional dependencies
npm install axios react-window react-window-infinite-loader
npm install -D tailwindcss postcss autoprefixerStep 2: Configure Tailwind CSS
# Initialize Tailwind
npx tailwindcss init -ptailwind.config.js
1module.exports = {
2 content: [
3 "./src/**/*.{js,jsx,ts,tsx}",
4 ],
5 theme: {
6 extend: {},
7 },
8 plugins: [],
9}src/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;Step 3: Project Structure
src/
├── components/ # Reusable UI pieces
├── hooks/ # Custom React logic
├── services/ # API communication
├── App.js # Main application
└── index.js # Entry pointCore Features Implementation
Feature 1: Smart Search Hook
What it does: Manages all the complex logic for searching books from multiple APIs.
1import { useState, useCallback, useRef } from "react";
2import axios from "axios";
3
4// Configuration - These numbers are carefully chosen!
5const RESULTS_PER_PAGE = 40; // Good balance of content vs speed
6const CACHE_TIME = 15 * 60 * 1000; // 15 minutes - fresh but efficient
7const REQUEST_TIMEOUT = 10000; // 10 seconds - don't wait forever
8
9/**
10 * Smart Cache System
11 * Think of this like your brain's memory - it remembers
12 * recent searches so you don't have to look them up again
13 */
14class BookCache {
15 constructor() {
16 this.cache = new Map(); // Storage for search results
17 this.hitCount = 0; // Successful cache retrievals
18 this.missCount = 0; // Times we had to search again
19 }
20
21 // Store a search result
22 set(key, data) {
23 this.cache.set(key, {
24 data,
25 timestamp: Date.now(), // When we stored it
26 });
27 }
28
29 // Get a search result (if it's still fresh)
30 get(key) {
31 const entry = this.cache.get(key);
32 if (!entry) {
33 this.missCount++;
34 return null;
35 }
36
37 // Check if the data is too old
38 if (Date.now() - entry.timestamp > CACHE_TIME) {
39 this.cache.delete(key);
40 this.missCount++;
41 return null;
42 }
43
44 this.hitCount++;
45 return entry.data;
46 }
47}
48
49// Our main search function
50export default function useBookSearch(query, pageNumber, onPageReset) {
51 // State management - these track what's happening
52 const [loading, setLoading] = useState(false);
53 const [error, setError] = useState(false);
54 const [books, setBooks] = useState([]);
55 const [hasMore, setHasMore] = useState(true);
56
57 const cache = useRef(new BookCache());
58
59 // The main search function
60 const fetchData = useCallback(async () => {
61 // Clean up the search query
62 const cleanQuery = query?.trim() || "fiction";
63 const cacheKey = `${cleanQuery}-${pageNumber}`;
64
65 try {
66 setLoading(true);
67 setError(false);
68
69 // Check if we already have this search cached
70 const cachedResult = cache.current.get(cacheKey);
71 if (cachedResult) {
72 setBooks(prev =>
73 pageNumber === 1
74 ? cachedResult.books
75 : [...prev, ...cachedResult.books]
76 );
77 setLoading(false);
78 return;
79 }
80
81 // Search both APIs at the same time
82 const [openLibraryResult, googleBooksResult] = await Promise.allSettled([
83 // Open Library API
84 axios.get("https://openlibrary.org/search.json", {
85 params: {
86 q: cleanQuery,
87 page: pageNumber,
88 limit: RESULTS_PER_PAGE,
89 },
90 timeout: REQUEST_TIMEOUT,
91 }),
92
93 // Google Books API (might fail, that's ok)
94 searchGoogleBooks(cleanQuery).catch(() => [])
95 ]);
96
97 // Process the results
98 let allBooks = [];
99
100 // Handle Open Library results
101 if (openLibraryResult.status === 'fulfilled') {
102 const olBooks = openLibraryResult.value.data.docs.map(book => ({
103 id: book.key,
104 title: book.title || 'Unknown Title',
105 authors: book.author_name || ['Unknown Author'],
106 thumbnail: book.cover_i
107 ? `https://covers.openlibrary.org/b/id/${book.cover_i}-M.jpg`
108 : null,
109 publishedDate: book.first_publish_year?.toString(),
110 source: 'openLibrary',
111 }));
112 allBooks.push(...olBooks);
113 }
114
115 // Handle Google Books results
116 if (googleBooksResult.status === 'fulfilled') {
117 allBooks.push(...googleBooksResult.value);
118 }
119
120 // Remove duplicates
121 const uniqueBooks = removeDuplicates(allBooks);
122
123 // Cache the results for next time
124 cache.current.set(cacheKey, {
125 books: uniqueBooks,
126 hasMore: uniqueBooks.length >= RESULTS_PER_PAGE
127 });
128
129 // Update the UI
130 setBooks(prev =>
131 pageNumber === 1
132 ? uniqueBooks
133 : [...prev, ...uniqueBooks]
134 );
135
136 } catch (error) {
137 console.error('Search failed:', error);
138 setError(true);
139 } finally {
140 setLoading(false);
141 }
142 }, [query, pageNumber]);
143
144 return { books, loading, error, hasMore, fetchData };
145}
146
147// Helper function to remove duplicate books
148function removeDuplicates(books) {
149 const seen = new Set();
150 return books.filter(book => {
151 const key = `${book.title}-${book.authors[0]}`.toLowerCase();
152 if (seen.has(key)) return false;
153 seen.add(key);
154 return true;
155 });
156}For Non-Technical Readers: This hook is like a super-smart librarian who:
- Remembers what you searched for recently
- Asks multiple libraries at once for better results
- Removes duplicate books
- Handles errors gracefully when libraries are closed
Feature 2: Virtual Scrolling for Performance
The Problem: Showing 1000+ books at once would freeze your browser.
The Solution: Only render books that are visible on screen.
1import { FixedSizeList as List } from "react-window";
2
3// Each book row component
4const BookRow = ({ index, style, data }) => {
5 const book = data[index];
6
7 return (
8 <div style={style} className="flex p-4 border-b hover:bg-gray-50">
9 {/* Book cover */}
10 <img
11 src={book.thumbnail}
12 alt={book.title}
13 className="w-16 h-24 object-cover rounded"
14 />
15
16 {/* Book details */}
17 <div className="ml-4 flex-1">
18 <h3 className="font-semibold text-lg">{book.title}</h3>
19 <p className="text-gray-600">by {book.authors.join(', ')}</p>
20 <p className="text-sm text-gray-500">
21 Published: {book.publishedDate}
22 </p>
23 </div>
24 </div>
25 );
26};
27
28// Virtual list component
29const BookList = ({ books }) => (
30 <List
31 height={600} // How tall the scrollable area is
32 itemCount={books.length} // Total number of books
33 itemSize={120} // Height of each book row
34 itemData={books} // The actual book data
35 >
36 {BookRow}
37 </List>
38);For Non-Technical Readers: Imagine a physical library where you can only see books on the current shelf you're looking at. As you move up or down, new shelves become visible. This saves energy because you're not trying to look at every book in the entire library at once.
Feature 3: Debounced Search Input
The Problem: If we search on every keystroke, we'd overwhelm the APIs.
The Solution: Wait for the user to stop typing before searching.
1import { useState, useEffect } from 'react';
2
3const SearchInput = ({ onSearch }) => {
4 const [query, setQuery] = useState('');
5 const [debouncedQuery, setDebouncedQuery] = useState('');
6
7 // Wait 300ms after user stops typing
8 useEffect(() => {
9 const timer = setTimeout(() => {
10 setDebouncedQuery(query);
11 }, 300);
12
13 // Clean up the timer if user types again
14 return () => clearTimeout(timer);
15 }, [query]);
16
17 // Trigger search when debounced query changes
18 useEffect(() => {
19 if (debouncedQuery) {
20 onSearch(debouncedQuery);
21 }
22 }, [debouncedQuery, onSearch]);
23
24 return (
25 <input
26 type="text"
27 value={query}
28 onChange={(e) => setQuery(e.target.value)}
29 placeholder="Search for books..."
30 className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500"
31 />
32 );
33};For Non-Technical Readers: This is like having a patient assistant who waits for you to finish speaking before they start looking up information, rather than running off to search after every word you say.
Feature 4: Error Handling & Recovery
1const ErrorBoundary = ({ error, onRetry }) => {
2 if (!error) return null;
3
4 return (
5 <div className="text-center py-12">
6 <div className="text-red-500 mb-4">
7 <h3 className="text-lg font-semibold">Oops! Something went wrong</h3>
8 <p className="text-sm">
9 We're having trouble connecting to our book sources.
10 </p>
11 </div>
12
13 <button
14 onClick={onRetry}
15 className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
16 >
17 Try Again
18 </button>
19
20 <p className="text-xs text-gray-500 mt-2">
21 If this persists, try refreshing the page
22 </p>
23 </div>
24 );
25};Performance Optimization
1. React.memo for Preventing Unnecessary Re-renders
1import { memo } from 'react';
2
3// This component only re-renders if its props actually change
4const BookCard = memo(({ book, onBookClick }) => {
5 return (
6 <div
7 className="p-4 border rounded-lg cursor-pointer hover:shadow-md"
8 onClick={() => onBookClick(book)}
9 >
10 <h3 className="font-semibold">{book.title}</h3>
11 <p className="text-gray-600">{book.authors.join(', ')}</p>
12 </div>
13 );
14});
15
16// Custom comparison function for even more control
17const BookCard = memo(({ book, onBookClick }) => {
18 // Component content here...
19}, (prevProps, nextProps) => {
20 // Only re-render if the book ID changed
21 return prevProps.book.id === nextProps.book.id;
22});Why This Matters: Without memo, every time the parent component updates (like when you type in the search box), ALL book cards would re-render. With memo, only cards that actually changed will re-render.
2. Smart Caching Strategy
1class AdvancedBookCache {
2 constructor() {
3 this.cache = new Map();
4 this.maxSize = 100; // Prevent memory bloat
5 this.accessOrder = new Map(); // Track usage for smart eviction
6 }
7
8 set(key, data) {
9 // Remove oldest entries if cache is full
10 if (this.cache.size >= this.maxSize) {
11 this.evictLeastRecentlyUsed();
12 }
13
14 this.cache.set(key, {
15 data,
16 timestamp: Date.now(),
17 accessCount: 1
18 });
19 this.accessOrder.set(key, Date.now());
20 }
21
22 get(key) {
23 const entry = this.cache.get(key);
24 if (!entry) return null;
25
26 // Check if expired (15 minutes)
27 if (Date.now() - entry.timestamp > 15 * 60 * 1000) {
28 this.cache.delete(key);
29 this.accessOrder.delete(key);
30 return null;
31 }
32
33 // Update access tracking
34 entry.accessCount++;
35 this.accessOrder.set(key, Date.now());
36 return entry.data;
37 }
38
39 evictLeastRecentlyUsed() {
40 // Remove 20% of oldest entries
41 const entries = Array.from(this.accessOrder.entries())
42 .sort(([,a], [,b]) => a - b)
43 .slice(0, Math.floor(this.maxSize * 0.2));
44
45 entries.forEach(([key]) => {
46 this.cache.delete(key);
47 this.accessOrder.delete(key);
48 });
49 }
50}3. Image Lazy Loading & Error Handling
1const BookCover = ({ src, alt, title }) => {
2 const [imageError, setImageError] = useState(false);
3 const [isLoading, setIsLoading] = useState(true);
4
5 const handleImageLoad = () => setIsLoading(false);
6 const handleImageError = () => {
7 setImageError(true);
8 setIsLoading(false);
9 };
10
11 if (imageError) {
12 return (
13 <div className="w-16 h-24 bg-gray-200 rounded flex items-center justify-center">
14 <BookIcon className="w-8 h-8 text-gray-400" />
15 </div>
16 );
17 }
18
19 return (
20 <div className="relative w-16 h-24">
21 {isLoading && (
22 <div className="absolute inset-0 bg-gray-200 animate-pulse rounded" />
23 )}
24 <img
25 src={src}
26 alt={alt}
27 loading="lazy" // Browser-level lazy loading
28 onLoad={handleImageLoad}
29 onError={handleImageError}
30 className={`w-full h-full object-cover rounded transition-opacity ${
31 isLoading ? 'opacity-0' : 'opacity-100'
32 }`}
33 />
34 </div>
35 );
36};Security Measures
1. Input Sanitization
1// Clean user input to prevent XSS attacks
2const sanitizeQuery = (input) => {
3 if (!input || typeof input !== 'string') return '';
4
5 return input
6 .trim() // Remove whitespace
7 .replace(/[<>\"'&]/g, '') // Remove dangerous characters
8 .replace(/\s+/g, ' ') // Normalize spaces
9 .slice(0, 200); // Limit length
10};
11
12// Sanitize API response data
13const sanitizeBookData = (book) => ({
14 id: sanitizeString(book.id),
15 title: sanitizeString(book.title) || 'Unknown Title',
16 authors: Array.isArray(book.authors)
17 ? book.authors.map(sanitizeString).filter(Boolean)
18 : ['Unknown Author'],
19 thumbnail: book.thumbnail ? validateUrl(book.thumbnail) : null,
20 publishedDate: sanitizeString(book.publishedDate),
21});
22
23const sanitizeString = (str) => {
24 if (!str || typeof str !== 'string') return '';
25 return str.replace(/[<>\"'&]/g, '').trim();
26};
27
28const validateUrl = (url) => {
29 try {
30 const parsedUrl = new URL(url);
31 return parsedUrl.protocol === 'https:' ? url : null;
32 } catch {
33 return null;
34 }
35};2. Safe External Link Handling
1const handleBookClick = (book) => {
2 let linkToOpen = null;
3
4 if (book.source === 'openLibrary') {
5 linkToOpen = `https://openlibrary.org${book.id}`;
6 } else if (book.source === 'google') {
7 linkToOpen = book.infoLink;
8 }
9
10 if (linkToOpen) {
11 // Security: prevent window.opener attacks
12 window.open(linkToOpen, '_blank', 'noopener,noreferrer');
13 }
14};3. API Request Security
1const makeSecureRequest = async (url, params) => {
2 return axios({
3 method: 'GET',
4 url,
5 params,
6 timeout: 10000, // Prevent hanging requests
7 headers: {
8 'Accept': 'application/json',
9 'User-Agent': 'BookSearchApp/1.0', // Identify your app
10 },
11 // Validate SSL certificates
12 httpsAgent: new https.Agent({
13 rejectUnauthorized: true
14 })
15 });
16};Complete App Integration
Putting it all together:
1import React, { useState, useCallback, useMemo } from 'react';
2import { FixedSizeList as List } from 'react-window';
3import InfiniteLoader from 'react-window-infinite-loader';
4import useBookSearch from './hooks/useBookSearch';
5import BookRow from './components/BookRow';
6import SearchControls from './components/SearchControls';
7
8function App() {
9 // State management
10 const [query, setQuery] = useState('');
11 const [debouncedQuery, setDebouncedQuery] = useState('');
12 const [pageNumber, setPageNumber] = useState(1);
13 const [sortBy, setSortBy] = useState('title');
14 const [sortOrder, setSortOrder] = useState('asc');
15
16 // Custom hook for book searching
17 const { books, loading, error, hasMore, retrySearch } = useBookSearch(
18 debouncedQuery,
19 pageNumber,
20 () => setPageNumber(1)
21 );
22
23 // Memoized sorted books to prevent unnecessary recalculations
24 const sortedBooks = useMemo(() => {
25 return [...books].sort((a, b) => {
26 let aValue, bValue;
27
28 if (sortBy === 'authors') {
29 aValue = (a.authors[0] || '').toLowerCase();
30 bValue = (b.authors[0] || '').toLowerCase();
31 } else {
32 aValue = (a[sortBy] || '').toLowerCase();
33 bValue = (b[sortBy] || '').toLowerCase();
34 }
35
36 return sortOrder === 'asc'
37 ? aValue.localeCompare(bValue)
38 : bValue.localeCompare(aValue);
39 });
40 }, [books, sortBy, sortOrder]);
41
42 // Event handlers
43 const handleSearch = useCallback((newQuery) => {
44 setQuery(newQuery);
45 // Debounce logic would go here
46 }, []);
47
48 const loadMoreItems = useCallback(() => {
49 if (!loading && hasMore) {
50 setPageNumber(prev => prev + 1);
51 }
52 }, [loading, hasMore]);
53
54 // Render functions
55 const renderRow = useCallback(({ index, style }) => (
56 <BookRow
57 book={sortedBooks[index]}
58 style={style}
59 onClick={handleBookClick}
60 />
61 ), [sortedBooks]);
62
63 return (
64 <div className="max-w-4xl mx-auto p-6">
65 {/* Search Interface */}
66 <SearchControls
67 query={query}
68 onSearchChange={handleSearch}
69 sortBy={sortBy}
70 sortOrder={sortOrder}
71 onSortChange={setSortBy}
72 onSortOrderChange={setSortOrder}
73 resultsCount={books.length}
74 />
75
76 {/* Results Display */}
77 {books.length > 0 && (
78 <div className="mt-6 border rounded-lg shadow-sm overflow-hidden">
79 <InfiniteLoader
80 isItemLoaded={(index) => index < books.length}
81 itemCount={hasMore ? books.length + 1 : books.length}
82 loadMoreItems={loadMoreItems}
83 >
84 {({ onItemsRendered, ref }) => (
85 <List
86 height={600}
87 width="100%"
88 itemCount={sortedBooks.length}
89 itemSize={160}
90 onItemsRendered={onItemsRendered}
91 ref={ref}
92 >
93 {renderRow}
94 </List>
95 )}
96 </InfiniteLoader>
97 </div>
98 )}
99
100 {/* Loading State */}
101 {loading && books.length === 0 && (
102 <div className="text-center py-12">
103 <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-4" />
104 <p>Searching for books...</p>
105 </div>
106 )}
107
108 {/* Error State */}
109 {error && (
110 <div className="text-center py-12">
111 <p className="text-red-500 mb-4">Something went wrong!</p>
112 <button
113 onClick={retrySearch}
114 className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
115 >
116 Try Again
117 </button>
118 </div>
119 )}
120
121 {/* No Results */}
122 {!loading && !error && books.length === 0 && debouncedQuery && (
123 <div className="text-center py-12">
124 <p className="text-gray-500">No books found for "{debouncedQuery}"</p>
125 <p className="text-sm text-gray-400">Try different search terms</p>
126 </div>
127 )}
128 </div>
129 );
130}
131
132export default App;Testing & Deployment
Unit Testing Example
1import { renderHook, waitFor } from '@testing-library/react';
2import useBookSearch from '../hooks/useBookSearch';
3
4describe('useBookSearch', () => {
5 test('should return loading state initially', () => {
6 const { result } = renderHook(() =>
7 useBookSearch('javascript', 1, () => {})
8 );
9
10 expect(result.current.loading).toBe(true);
11 expect(result.current.books).toEqual([]);
12 expect(result.current.error).toBe(false);
13 });
14
15 test('should fetch books successfully', async () => {
16 const { result } = renderHook(() =>
17 useBookSearch('javascript', 1, () => {})
18 );
19
20 await waitFor(() => {
21 expect(result.current.loading).toBe(false);
22 expect(result.current.books.length).toBeGreaterThan(0);
23 expect(result.current.error).toBe(false);
24 });
25 });
26});Deployment to Netlify
1. Build the project:
npm run build2. Create a _redirects file in the public/ folder:
/* /index.html 2003. Deploy to Netlify:
✔ Connect your GitHub repository
✔ Set build command: npm run build
✔ Set publish directory: build
✔ Deploy!
Lessons Learned
Technical Insights
✔ Performance Matters Early: Don't wait until you have performance problems to think about optimization.
✔ User Experience First: Features like loading states and error handling aren't afterthoughts - they're core to good UX.
✔ Cache Strategy is Critical: A good caching strategy can make your app feel 10x faster.
✔ Error Handling is Hard: It's not just about try/catch - you need to think about user recovery paths.
Development Best Practices
✔ Start Simple: We began with basic search and added features incrementally.
✔ Document Your Why: The best comments explain WHY you made a decision, not what the code does.
✔ Test Early and Often: Don't wait until the end to start testing.
✔ Security by Design: Input sanitization and secure API calls should be built in from day one.
What We'd Do Differently
✔ TypeScript from the Start: Would have caught many bugs earlier.
✔ More Granular Components: Some of our components became too large and complex.
✔ Better Error Categorization: Different types of errors should be handled differently.
✔ Performance Monitoring: Should have added analytics to track real-world performance.
Conclusion
Building this book search app taught us that modern web development is about balancing multiple concerns:
- Performance: Users expect instant responses
- Accessibility: Everyone should be able to use your app
- Security: Protect users from malicious content
- Maintainability: Future developers (including yourself) need to understand your code
The key is to start simple and iterate. Don't try to build everything at once. Focus on the core user experience first, then add features that genuinely improve that experience.
Most importantly, remember that code is written for humans to read, not just for computers to execute. Clear, well-documented code is a gift to your future self and your teammates.
Share this article
Send it to someone who would find it useful.