This document analyzes real-world React hooks and patterns from the CollectTracker application, demonstrating practical implementations of React concepts.
Project Repository: [Link to CollectTracker Repository]
- Form Handling with Hooks
- List Rendering Patterns
- Modal Implementation
- Search and Filter Functionality
- Component Structure and Routing
File: [client/src/components/AddCollectionForm.js](Link to file)
The AddCollectionForm component demonstrates several important React patterns for form handling:
import React, { useState } from "react";
import "./AddCollectionForm.css";
import API_BASE_URL from "../config";
const AddCollectionForm = ({ onCollectionAdded }) => {
// State Management
const [isModalOpen, setIsModalOpen] = useState(false);
const [formData, setFormData] = useState({
name: "",
type: "generic",
customType: "",
});
// Collection types for dropdown
const collectionTypes = [
{ value: "generic", label: "Generic" },
{ value: "books", label: "Books" },
{ value: "records", label: "Records" },
{ value: "custom", label: "Custom" },
];
// Form submission handler
const handleSubmit = async (e) => {
e.preventDefault();
try {
const submitData = {
name: formData.name,
type: formData.type === "custom" ? formData.customType : formData.type,
};
const response = await fetch(`${API_BASE_URL}/collections`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(submitData),
});
const data = await response.json();
// Notify parent component and reset form
onCollectionAdded(data);
setIsModalOpen(false);
setFormData({ name: "", type: "generic", customType: "" });
} catch (error) {
console.error("Error adding collection:", error);
}
};
// Conditional rendering for the modal and custom type field
return (
<div className="add-collection-container">
<button className="add-button" onClick={() => setIsModalOpen(true)}>
+ Add Collection
</button>
{isModalOpen && (
<div className="modal-overlay">
<div className="modal">
{/* Form content */}
<form onSubmit={handleSubmit}>
{/* Form fields */}
{/* Conditional rendering example */}
{formData.type === "custom" && (
<div className="form-group">
<label>Custom Type Name:</label>
<input
type="text"
value={formData.customType}
onChange={(e) =>
setFormData({ ...formData, customType: e.target.value })
}
required
/>
</div>
)}
{/* Form buttons */}
</form>
</div>
</div>
)}
</div>
);
};-
useState Hook for Form State Management:
- Uses a single state object (
formData) to manage multiple form fields - Initializes form with default values
- Demonstrates proper state updates with the spread operator to preserve other field values
- Uses a single state object (
-
Controlled Components Pattern:
- Form inputs are fully controlled by React state
- Each input's value comes from state and is updated through onChange handlers
- This ensures React is the "single source of truth" for form data
-
Conditional Rendering:
- Modal dialog only renders when
isModalOpenstate is true - Custom type input field only appears when 'custom' type is selected
- Uses the logical AND (
&&) pattern for conditional rendering
- Modal dialog only renders when
-
Form Submission with Async/Await:
- Prevents default form submission behavior
- Uses async/await for clean API interaction
- Properly handles form submission and resets form state afterward
-
Prop Callbacks for Parent Communication:
- Uses the
onCollectionAddedcallback to notify parent component of changes - Demonstrates the unidirectional data flow pattern in React
- Uses the
-
Event Handler Patterns:
- Inline arrow functions for simple state updates
- Separate handler function for complex operations (form submission)
- Proper event object usage with
preventDefault()
-
State Reset Pattern:
- Resets form to initial state after successful submission
- Closes modal upon completion
File: [client/src/components/CollectionList.js](Link to file)
The CollectionList component demonstrates advanced patterns for rendering lists with filtering, sorting, and CRUD operations:
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import "./CollectionList.css";
import API_BASE_URL from "../config";
const CollectionList = () => {
const navigate = useNavigate();
// Multiple state variables for different concerns
const [collections, setCollections] = useState([]);
const [filteredCollections, setFilteredCollections] = useState([]);
const [searchTerm, setSearchTerm] = useState("");
const [sortOption, setSortOption] = useState("name-asc");
const [editingCollection, setEditingCollection] = useState(null);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showAddModal, setShowAddModal] = useState(false);
const [collectionToDelete, setCollectionToDelete] = useState(null);
// Data fetching with useEffect
useEffect(() => {
fetchCollections();
}, []);
const fetchCollections = async () => {
try {
const response = await fetch(`${API_BASE_URL}/collections`);
const data = await response.json();
setCollections(data);
} catch (error) {
console.error("Error fetching collections:", error);
}
};
// Derived state with useEffect for filtering and sorting
useEffect(() => {
const filtered = Array.isArray(collections)
? collections.filter(
(collection) =>
collection.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
collection.type.toLowerCase().includes(searchTerm.toLowerCase())
)
: [];
const sortedAndFiltered = getSortedCollections(filtered);
setFilteredCollections(sortedAndFiltered);
}, [searchTerm, collections, sortOption]);
// Pure function for sorting collections
const getSortedCollections = (collections) => {
const sorted = [...collections];
switch (sortOption) {
case "name-asc":
return sorted.sort((a, b) => a.name.localeCompare(b.name));
case "name-desc":
return sorted.sort((a, b) => b.name.localeCompare(a.name));
case "newest":
return sorted.sort(
(a, b) => new Date(b.created_at) - new Date(a.created_at)
);
case "oldest":
return sorted.sort(
(a, b) => new Date(a.created_at) - new Date(b.created_at)
);
case "most-items":
return sorted.sort((a, b) => b.item_count - a.item_count);
case "least-items":
return sorted.sort((a, b) => a.item_count - b.item_count);
default:
return sorted;
}
};
// JSX with list rendering and conditional content
return (
<div className="collections-container">
{/* Search and sort controls */}
<div className="search-and-sort">
<input
type="text"
placeholder="Search collections..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<select
value={sortOption}
onChange={(e) => setSortOption(e.target.value)}
>
<option value="name-asc">Name (A-Z)</option>
<option value="name-desc">Name (Z-A)</option>
{/* Additional options */}
</select>
</div>
{/* Collections grid with map function */}
<div className="collections-grid">
{filteredCollections.map((collection) => (
<div
key={collection.id}
className="collection-card"
onClick={() => handleCollectionClick(collection.id)}
>
<div className="card-header">
{editingCollection?.id === collection.id ? (
// Edit mode with inline form
<form
onSubmit={handleSaveEdit}
onClick={(e) => e.stopPropagation()}
>
<input
value={editingCollection.name}
onChange={(e) =>
setEditingCollection({
...editingCollection,
name: e.target.value,
})
}
autoFocus
/>
<button type="submit">Save</button>
<button
type="button"
onClick={() => setEditingCollection(null)}
>
Cancel
</button>
</form>
) : (
// Display mode
<>
<h3>{collection.name}</h3>
<div
className="card-actions"
onClick={(e) => e.stopPropagation()}
>
<button onClick={(e) => handleEdit(e, collection)}>
Edit
</button>
<button
onClick={() => {
setCollectionToDelete(collection);
setShowDeleteModal(true);
}}
>
Delete
</button>
</div>
</>
)}
</div>
</div>
))}
{/* Static "Add New" card */}
<div
className="collection-card add-card"
onClick={() => setShowAddModal(true)}
>
<div className="add-icon">+</div>
<p>Add New Collection</p>
</div>
</div>
{/* Modals for add and delete operations */}
</div>
);
};-
Multiple useState Hooks for Complex State Management:
- Separates concerns with multiple state variables
- Uses appropriate state types for different data (arrays, booleans, objects)
- Demonstrates when to use separate state variables vs. combined objects
-
useEffect for Data Fetching:
- Fetches data on component mount with empty dependency array
- Properly separates the fetch logic into a named function
- Handles errors appropriately
-
useEffect for Derived State:
- Calculates filtered and sorted collections when dependencies change
- Avoids unnecessary state updates with proper dependency array
- Demonstrates the "computed properties" pattern in React
-
List Rendering with Keys:
- Uses the map function to render collection cards
- Properly assigns unique keys to list items
- Demonstrates efficient list rendering
-
Conditional Rendering Patterns:
- Ternary operator for edit/display mode switching
- Logical AND (
&&) for modal rendering - Fragment syntax (
<>...</>) for grouping elements without extra DOM nodes
-
Event Propagation Control:
- Uses
e.stopPropagation()to prevent click events from bubbling - Demonstrates understanding of React's event system
- Prevents unintended navigation when clicking edit/delete buttons
- Uses
-
Optimistic UI Updates:
- Updates the UI immediately after user actions
- Maintains consistency between server and client state
- Provides immediate feedback to user actions
-
Pure Function Pattern:
getSortedCollectionsis a pure function that doesn't depend on component state- Makes the sorting logic testable and reusable
- Separates data transformation from rendering logic
-
Routing Integration with useNavigate:
- Uses React Router's
useNavigatehook for programmatic navigation - Demonstrates integration between component state and routing
- Uses React Router's
-
Inline Edit Pattern:
- Allows editing collection names directly in the list
- Toggles between display and edit modes
- Maintains edit state separately from the main data
This component demonstrates advanced React patterns for list management, including filtering, sorting, and inline editing, while maintaining clean separation of concerns.
File: [client/src/components/PhotoGalleryModal.js](Link to file)
The PhotoGalleryModal component demonstrates advanced React patterns for modal interfaces and image handling:
import React, { useState, useEffect, useCallback } from "react";
const PhotoGalleryModal = ({ item, onClose, onRefresh }) => {
// State management for photos and UI
const [photos, setPhotos] = useState([]);
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
const [isUploading, setIsUploading] = useState(false);
// Fetch photos with useCallback
const fetchPhotos = useCallback(async () => {
try {
const response = await fetch(
`${API_BASE_URL}/collections/items/${item.id}/photos`
);
if (response.ok) {
const data = await response.json();
setPhotos(data);
}
} catch (error) {
console.error("Error fetching photos:", error);
}
}, [item.id]);
// Initialize photos on component mount
useEffect(() => {
fetchPhotos();
}, [fetchPhotos]);
// Photo navigation with circular array pattern
const nextPhoto = () => {
setCurrentPhotoIndex((prev) => (prev + 1) % photos.length);
};
const previousPhoto = () => {
setCurrentPhotoIndex((prev) => (prev - 1 + photos.length) % photos.length);
};
return (
<div className="photo-gallery-modal-overlay">
<div className="photo-gallery-modal">
{/* Modal content with navigation */}
<div className="gallery-content">
{photos.length > 0 ? (
<div className="gallery-viewer">
{/* Navigation buttons and current photo */}
</div>
) : (
<div className="no-photos">
<p>No photos in gallery</p>
</div>
)}
</div>
</div>
</div>
);
};-
useCallback for Memoized Functions:
- Uses
useCallbackto memoize thefetchPhotosfunction - Prevents unnecessary re-creation of the function on re-renders
- Properly specifies dependencies (
item.id) to update when needed
- Uses
-
useEffect with Memoized Callback:
- Uses the memoized
fetchPhotosfunction in the dependency array - Ensures the effect only runs when the callback changes
- Demonstrates proper dependency management
- Uses the memoized
-
Modal Pattern with Overlay:
- Creates a modal dialog with background overlay
- Provides clear visual separation from the main content
- Includes proper close button and header
-
Carousel/Gallery Pattern:
- Implements next/previous navigation for photos
- Uses modulo arithmetic for circular navigation
- Handles edge cases (no photos, single photo)
-
Loading State Management:
- Tracks upload state with
isUploading - Provides visual feedback during async operations
- Disables controls during uploads to prevent concurrent operations
- Tracks upload state with
-
Conditional Rendering for Empty States:
- Shows different UI when no photos are available
- Provides clear user feedback for empty states
- Maintains good UX in all scenarios
-
File Upload Pattern:
- Handles multiple file uploads
- Processes files sequentially with async/await
- Updates UI after successful uploads
-
Index Management After Deletions:
- Intelligently adjusts the current photo index after deletion
- Prevents out-of-bounds errors when deleting the last photo
- Maintains a good user experience during destructive operations
-
Props for Component Communication:
- Uses
onClosefor modal dismissal - Uses
onRefreshto notify parent of data changes - Demonstrates proper component communication patterns
- Uses
This component showcases advanced React patterns for building interactive modal interfaces with image handling capabilities, proper state management, and optimized rendering.
File: [client/src/components/SearchSortControls.js](Link to file)
The SearchSortControls component demonstrates React patterns for creating reusable, controlled components:
import React from "react";
import "./SearchSortControls.css";
const SearchSortControls = ({
searchTerm,
onSearchChange,
sortOption,
onSortChange,
sortOptions,
searchPlaceholder,
}) => {
return (
<div className="search-and-sort">
{/* Search input section */}
<div className="search-container">
<input
type="text"
placeholder={searchPlaceholder}
value={searchTerm}
onChange={(e) => onSearchChange(e.target.value)}
className="search-input"
/>
</div>
{/* Sort selection dropdown */}
<div className="sort-container">
<select
value={sortOption}
onChange={(e) => onSortChange(e.target.value)}
className="sort-select"
>
{sortOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
</div>
);
};-
Controlled Component Pattern:
- Both search input and sort select are fully controlled components
- Values come from props and changes are handled by callback props
- Maintains the parent component as the single source of truth
-
Presentational Component Pattern:
- Component focuses purely on UI rendering, not business logic
- All data and behavior are passed through props
- Makes the component highly reusable across different contexts
-
Prop Destructuring:
- Uses JavaScript destructuring for clean prop access
- Makes the component signature clear and readable
- Provides visual documentation of required props
-
Callback Props Pattern:
- Uses function props (
onSearchChange,onSortChange) for communication - Passes data up to parent components
- Follows React's unidirectional data flow principle
- Uses function props (
-
Configurable Component Pattern:
- Accepts customization props like
searchPlaceholder - Takes dynamic data like
sortOptionsto render different options - Maintains flexibility while providing a consistent interface
- Accepts customization props like
-
Event Handler Delegation:
- Delegates event handling to parent components
- Processes event data before passing to callbacks (
e.target.value) - Keeps the component focused on its presentation role
-
List Rendering with Keys:
- Uses the
mapfunction with proper keys for option rendering - Ensures efficient updates when options change
- Follows React best practices for list rendering
- Uses the
This component demonstrates how to create flexible, reusable UI components that can be composed into larger applications. It separates presentation from logic and provides a clean interface for parent components to control its behavior.
File: [client/src/App.js](Link to file)
The App component demonstrates React patterns for application structure and routing:
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import "./App.css";
import MainLayout from "./components/MainLayout";
import CollectionPage from "./components/CollectionPage";
function App() {
return (
<Router>
<div className="App">
{/* Application Header */}
<header className="App-header">
<h1>CollectTracker</h1>
</header>
{/* Route Definitions */}
<Routes>
<Route path="/" element={<MainLayout />} />
<Route path="/collection/:id" element={<CollectionPage />} />
</Routes>
</div>
</Router>
);
}
export default App;-
React Router Integration:
- Uses React Router v6 for declarative routing
- Wraps the application in a
Routercomponent - Defines routes with the
RoutesandRoutecomponents
-
Route Parameter Pattern:
- Uses dynamic route parameters (
:id) for collection pages - Enables navigation to specific resources
- Follows RESTful URL structure
- Uses dynamic route parameters (
-
Component Composition:
- Composes the application from smaller, focused components
- Separates concerns between routing and content
- Maintains a clean component hierarchy
-
Declarative UI Structure:
- Uses JSX to declaratively define the application structure
- Clearly separates header and content areas
- Makes the component hierarchy easy to understand
-
Consistent Layout Pattern:
- Maintains consistent header across all routes
- Provides a container for route-specific content
- Creates a cohesive user experience
-
Function Component Pattern:
- Uses modern function component syntax
- Avoids unnecessary class components
- Follows React best practices for component definition
This component demonstrates how to structure a React application with routing, providing a solid foundation for navigation between different views while maintaining a consistent user interface.