Skip to main content

Command Palette

Search for a command to run...

Building a Responsive Photo Gallery with React and Unsplash API: A Complete Guide

Updated
7 min read
Building a Responsive Photo Gallery with React and Unsplash API: A Complete Guide
O
Oyinkansola Shoroye is a software engineer with a passion for crafting delightful user experiences through clean and efficient code. She advocates for accessibility and inclusive web experiences, and she loves exploring emerging technologies and frameworks. On Hashnode, she will share her ideas and insights through engaging blog posts and tutorials. Connect, learn, and grow together with her as you shape the future of web development. Feel free to reach out to her for collaborations or questions!

In this comprehensive tutorial, we'll build a fully responsive photo gallery using React and the Unsplash API. I'll explain every key concept and decision behind our implementation, with special focus on the core components: Gallery, Pagination, Search, and App.

The complete code is available on GitHub Repository

Project Setup

Creating the React App

We'll use Vite for our React project because it offers:

  • Faster development server start times

  • Optimized production builds

  • Native ES modules support

npm create vite@latest unsplash-gallery --template react
cd unsplash-gallery

Installing Dependencies

npm install axios react-icons
  • axios: For making HTTP requests to the Unsplash API (better error handling than fetch)

  • react-icons: Provides easy-to-use icons (better than importing font libraries)

Environment Configuration

Create a .env file in your project root:

VITE_UNSPLASH_ACCESS_KEY=your_access_key_here

Why environment variables?

  • Keeps API keys out of source control

  • Allows different configurations for development/production

  • Follows security best practices

Component Architecture

Our app will consist of these main components:

  1. App: Main container and state management

  2. Gallery: Displays images in a responsive grid

  3. Pagination: Handles page navigation

  4. SearchBar: User input for searching images

  5. ImageCard: Individual image display component

src/
├── components/
│   ├── Gallery/
│   ├── Pagination/
│   ├── SearchBar/
│   └── ImageCard/
├── App.jsx
├── main.jsx
└── styles/

src/components/Gallery/Gallery.jsx:

import { FaSpinner } from 'react-icons/fa';
import ImageCard from '../ImageCard/ImageCard';
import './Gallery.css';

export default function Gallery({ images, isLoading, error }) {
  // Loading state - shows spinner animation
  if (isLoading) {
    return (
      <div className="loading">
        <FaSpinner className="spinner" />
        <p>Loading images...</p>
      </div>
    );
  }

  // Error state - shows error message
  if (error) {
    return <div className="error">Error: {error}</div>;
  }

  // Empty state - when no images are found
  if (images.length === 0) {
    return <div className="empty">No images found. Try another search!</div>;
  }

  // Main gallery grid
  return (
    <div className="gallery">
      {images.map((image) => (
        <ImageCard key={image.id} image={image} />
      ))}
    </div>
  );
}

Key Features Explained:

  1. Conditional Rendering:

    • Shows different UI states (loading, error, empty, content)

    • Provides better user experience than empty screens

  2. ImageCard Component:

    • Separates individual image display logic

    • Makes Gallery component more maintainable

  3. Performance Optimization:

    • Uses key prop for efficient React rendering

    • Loading state prevents layout shifts

  4. CSS Grid Layout:

    • Responsive design handled in CSS (shown below)

src/components/Gallery/Gallery.css:

.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 1rem;
  padding: 1rem;
  max-width: 1200px;
  margin: 0 auto;
  width: 100%;
}

@media (max-width: 768px) {
  .gallery {
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  }
}

@media (max-width: 480px) {
  .gallery {
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: 0.75rem;
    padding: 0.75rem;
  }
}

.loading {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 3rem;
  color: #3a86ff;
}

.spinner {
  font-size: 2rem;
  animation: spin 1s linear infinite;
  margin-bottom: 1rem;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

.error, .empty {
  text-align: center;
  padding: 2rem;
  color: #ff3333;
}

.empty {
  color: #666;
}

Why CSS Grid?

  • Creates flexible layouts that adapt to screen size

  • auto-fill automatically adjusts number of columns

  • minmax() ensures cards don't get too small or large

Implementing Pagination

src/components/Pagination/Pagination.jsx:

import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
import './Pagination.css';

export default function Pagination({ currentPage, totalPages, onPageChange }) {
  // Don't render if only one page exists
  if (totalPages <= 1) return null;

  return (
    <div className="pagination">
      <button
        onClick={() => onPageChange(currentPage - 1)}
        disabled={currentPage === 1}
        className="pagination-button"
        aria-label="Previous page"
      >
        <FaChevronLeft />
      </button>
      
      <span className="page-info">
        Page {currentPage} of {totalPages}
      </span>
      
      <button
        onClick={() => onPageChange(currentPage + 1)}
        disabled={currentPage === totalPages}
        className="pagination-button"
        aria-label="Next page"
      >
        <FaChevronRight />
      </button>
    </div>
  );
}

Key Features Explained:

  1. Conditional Rendering:

    • Only shows pagination when needed (totalPages > 1)
  2. Accessibility:

    • aria-label helps screen readers

    • Disabled states for better UX

  3. Props Design:

    • Receives current state from parent

    • Callback for page changes (keeps logic in parent)

  4. Visual Feedback:

    • Hover states (in CSS)

    • Clear current page indication

src/components/Pagination/Pagination.css:

.pagination {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 1rem;
  margin: 2rem 0;
  padding: 0 1rem;
}

.pagination-button {
  background-color: #3a86ff;
  color: white;
  border: none;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background-color 0.3s;
}

.pagination-button:hover:not(:disabled) {
  background-color: #2667cc;
}

.pagination-button:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}

.page-info {
  font-size: 0.9rem;
  color: #333;
}

Why this pagination approach?

  • Simple and intuitive navigation

  • Works well with API pagination

  • Mobile-friendly design

Creating the Search Functionality

src/components/SearchBar/SearchBar.jsx:

import { useState } from 'react';
import { FaSearch } from 'react-icons/fa';
import './SearchBar.css';

export default function SearchBar({ onSearch }) {
  const [searchTerm, setSearchTerm] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (searchTerm.trim()) {
      onSearch(searchTerm);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="search-form">
      <div className="search-container">
        <FaSearch className="search-icon" />
        <input
          type="text"
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          placeholder="Search for images..."
          className="search-input"
        />
        <button type="submit" className="search-button">
          Search
        </button>
      </div>
    </form>
  );
}

Key Features Explained:

  1. Controlled Input:

    • React state manages input value

    • Allows validation before submission

  2. Form Submission:

    • Proper form handling (not just button click)

    • Works with Enter key press

  3. Validation:

    • Checks for empty/whitespace-only searches

    • Prevents unnecessary API calls

  4. Visual Design:

    • Icon for better affordance

    • Responsive layout (changes on mobile)

src/components/SearchBar/SearchBar.css:

.search-form {
  max-width: 800px;
  margin: 2rem auto;
  padding: 0 1rem;
}

.search-container {
  display: flex;
  border-radius: 30px;
  overflow: hidden;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.search-input {
  flex: 1;
  padding: 1rem 1rem 1rem 3rem;
  border: none;
  font-size: 1rem;
  font-family: 'Poppins', sans-serif;
}

.search-input:focus {
  outline: none;
}

.search-button {
  background-color: #3a86ff;
  color: white;
  border: none;
  padding: 0 2rem;
  font-size: 1rem;
  cursor: pointer;
  transition: background-color 0.3s;
}

.search-button:hover {
  background-color: #2667cc;
}

@media (max-width: 600px) {
  .search-container {
    flex-direction: column;
    border-radius: 8px;
  }
  
  .search-input {
    padding: 1rem;
  }
  
  .search-button {
    padding: 1rem;
    width: 100%;
  }
}

Why this search implementation?

  • Follows standard form patterns

  • Provides immediate visual feedback

  • Works across devices

App Component: Bringing It All Together

src/App.jsx:

import { useState, useEffect } from 'react';
import axios from 'axios';
import Header from './components/Header/Header';
import SearchBar from './components/SearchBar/SearchBar';
import Gallery from './components/Gallery/Gallery';
import Pagination from './components/Pagination/Pagination';
import './App.css';

export default function App() {
  // State management
  const [images, setImages] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);
  const [searchTerm, setSearchTerm] = useState('nature');
  const [page, setPage] = useState(1);
  const [totalPages, setTotalPages] = useState(0);

  const apiKey = import.meta.env.VITE_UNSPLASH_ACCESS_KEY;

  // API fetch function
  const fetchImages = async () => {
    try {
      setIsLoading(true);
      setError(null);
      
      const response = await axios.get('https://api.unsplash.com/search/photos', {
        params: {
          query: searchTerm,
          page,
          per_page: 12, // Optimal number for grid layout
          client_id: apiKey
        }
      });
      
      setImages(response.data.results);
      setTotalPages(response.data.total_pages);
    } catch (err) {
      setError(err.message || 'Failed to fetch images');
    } finally {
      setIsLoading(false);
    }
  };

  // Fetch images when search term or page changes
  useEffect(() => {
    if (apiKey) {
      fetchImages();
    } else {
      setError('Unsplash API key is missing');
      setIsLoading(false);
    }
  }, [searchTerm, page, apiKey]);

  // Handler for search submission
  const handleSearch = (term) => {
    setSearchTerm(term);
    setPage(1); // Reset to first page for new searches
  };

  // Handler for page changes
  const handlePageChange = (newPage) => {
    setPage(newPage);
    window.scrollTo({ top: 0, behavior: 'smooth' });
  };

  return (
    <div className="app">
      <Header />
      <SearchBar onSearch={handleSearch} />
      <Gallery images={images} isLoading={isLoading} error={error} />
      <Pagination
        currentPage={page}
        totalPages={totalPages}
        onPageChange={handlePageChange}
      />
    </div>
  );
}

Key Features Explained:

  1. State Management:

    • Centralizes all application state

    • Includes loading and error states

  2. API Integration:

    • Uses axios for better error handling

    • Implements proper loading states

  3. Component Composition:

    • Clean separation of concerns

    • Props drilling for communication

  4. User Experience:

    • Scrolls to top on page change

    • Resets page on new searches

  5. Error Handling:

    • Checks for API key presence

    • Gracefully handles API errors

Final Thoughts

This implementation provides a solid foundation for a production-ready photo gallery with:

  1. Responsive Design: Works on all screen sizes

  2. Performance Optimizations: Lazy loading, efficient rendering

  3. Robust Error Handling: Graceful degradation

  4. Maintainable Architecture: Well-organized components

  5. Good Practices: Environment variables, accessibility

1 views