DEV Community

Cover image for React Movie App
King Coe
King Coe

Posted on

React Movie App

🎬 How I Built a React Movie Search App (Technical Breakdown)

By King Code

πŸ‘‹ Introduction

In this post, I want to share how I built Simple Movie Search, a responsive React application that interfaces with the OMDb API.

My goal for this project was to demonstrate:

  • Async/Await Data Fetching: Handling API responses and loading states.
  • State Management: Using useState for search terms, movie lists, and UI flags.
  • Robust Error Handling: managing "404" images and empty search results.
  • Responsive UI: Building a CSS Grid system that works on mobile and desktop.

⚑ Step 0: Prerequisites & Setup

Before writing any code, we need to set up our environment.

1. Check for Node.js

You need Node.js installed to run React. Open your terminal (Command Prompt or PowerShell) and type:

node -v
Enter fullscreen mode Exit fullscreen mode

If you see a version number (e.g., v18.16.0): You are good to go!

If you see an error: Download and install it from nodejs.org.

2. Create the Project

We will use Vite to create our React app because it is faster and more modern than the old Create React App.

Run these commands one by one in your terminal:

# 1. Go to your Desktop (so you can find the folder easily)
cd Desktop

# 2. Create the project (we'll call it 'movie-search')
npm create vite@latest movie-search -- --template react

# 3. Enter the new folder
cd movie-search

# 4. Install the dependencies
npm install

# 5. Start the development server
npm run dev
Enter fullscreen mode Exit fullscreen mode

If everything worked, you should see a "Local" URL (like http://localhost:5173). Open that in your browser, and let's start coding!


πŸ› οΈ Step 1: The Foundation (State Management)

The first step was setting up the "brain" of the application. I needed to track several pieces of state to make the UI interactive.

I introduced a hasSearched boolean flag. This solves a specific UX problem: it allows the app to distinguish between a user who just arrived (clean slate) versus a user who searched but found nothing (error message).

import { useState } from 'react'
import './App.css'

function App() {
  // 1. The list of movies from the API
  const [movies, setMovies] = useState([])

  // 2. The current text in the search bar
  const [searchTerm, setSearchTerm] = useState('')

  // 3. The specific movie currently open in the popup (null = closed)
  const [selectedMovie, setSelectedMovie] = useState(null)

  // 4. UX Flags
  const [hasSearched, setHasSearched] = useState(false)
  const [lastSearch, setLastSearch] = useState('')

  const API_KEY = 'YOUR_API_KEY'
Enter fullscreen mode Exit fullscreen mode

πŸ” Step 2: The Search Logic & Filtering

Interacting with external APIs often requires cleaning up data before showing it to the user.

The OMDb API returns movies with "N/A" as the poster URL if no image exists. Instead of showing a broken image icon, I used .filter() to remove these results entirely before updating the state.

  const searchMovies = async (title) => {
    setHasSearched(true)
    setLastSearch(title) // Freezes the search term for the "No results" message

    const response = await fetch(`https://www.omdbapi.com/?apikey=${API_KEY}&s=${title}`)
    const data = await response.json()

    if (data.Search) {
      // πŸš€ Optimization: Filter out movies with missing posters immediately
      const validMovies = data.Search.filter((movie) => movie.Poster !== 'N/A')
      setMovies(validMovies)
    } else {
      setMovies([])
    }
  }
Enter fullscreen mode Exit fullscreen mode

🎨 Step 3: Handling Broken Images in the UI

Even with the filter above, sometimes a valid URL leads to a 404 error (dead link).

To fix this, I added an inline onError handler to the <img> tag. If the browser fails to load an image, it immediately hides the entire card. This ensures the grid always looks professional.

<img
  src={movie.Poster}
  alt={movie.Title}
  // If the image fails to load, hide the entire card container
  onError={(e) => e.target.parentElement.style.display = 'none'} 
/>
Enter fullscreen mode Exit fullscreen mode

πŸ“± Step 4: Responsive Grid Layout

I used CSS Grid to create a layout that adapts to any screen size without needing complex @media queries for every breakpoint.

The minmax(200px, 1fr) function ensures that cards are at least 200px wide, but stretch to fill the available space if needed.

.movie-grid {
  display: grid;
  /* The "Magical" Responsive Line */
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 30px;
}
Enter fullscreen mode Exit fullscreen mode

✨ Step 5: The "Modal" Detail View

To add depth to the app, I implemented a popup modal that fetches full details (Plot, Director, Actors) when a user clicks a card.

I added e.stopPropagation() to the modal content container. This creates a polished UX: clicking the dark background closes the modal, but clicking the white content box does nothing.

  // 1. The Fetch Function
  const fetchMovieDetail = async (id) => {
    const response = await fetch(`https://www.omdbapi.com/?apikey=${API_KEY}&i=${id}`)
    const data = await response.json()
    setSelectedMovie(data)
  }

  // 2. The UI Logic (Added to the return statement)
  {selectedMovie && (
    <div className="modal-overlay" onClick={() => setSelectedMovie(null)}>
      {/* stopPropagation prevents clicks inside the box from closing it */}
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        <button className="close-btn" onClick={() => setSelectedMovie(null)}>Γ—</button>

        <div className="modal-header">
          <img src={selectedMovie.Poster} alt={selectedMovie.Title} />
          <div className="modal-info">
            <h2>{selectedMovie.Title}</h2>
            <p>{selectedMovie.Plot}</p>
          </div>
        </div>
      </div>
    </div>
  )}
Enter fullscreen mode Exit fullscreen mode

πŸš€ Conclusion

This project was a great exercise in clean React architecture. By separating the search logic from the display logic, and handling edge cases like missing images and empty states, I was able to build a robust little application in under 100 lines of code.

Key Takeaways:

  1. Always clean API data before rendering (filter).
  2. Plan for empty states ("No results found").
  3. Use CSS Grid for effortless responsiveness.

Thanks for reading!

PSA. For the lazy:

πŸ“‚ Full Source Code

Here is the complete App.jsx file combining all the steps above.

import { useState } from 'react'
import './App.css'

function App() {
  // --- STATE MANAGEMENT ---
  const [movies, setMovies] = useState([]) 
  const [searchTerm, setSearchTerm] = useState('') 
  const [selectedMovie, setSelectedMovie] = useState(null) // null = modal closed
  const [hasSearched, setHasSearched] = useState(false)    // Tracks if a search has happened
  const [lastSearch, setLastSearch] = useState('')         // Stores the term for error messages

  // πŸ”‘ API CONFIGURATION (Replace with your own key)
  const API_KEY = 'YOUR_OMDB_API_KEY'

  // --- FUNCTION 1: SEARCH MOVIES ---
  const searchMovies = async (title) => {
    setHasSearched(true) 
    setLastSearch(title) 

    // Fetch data from OMDb API
    const response = await fetch(`https://www.omdbapi.com/?apikey=${API_KEY}&s=${title}`)
    const data = await response.json()

    if (data.Search) {
      // πŸš€ Filter out movies with missing images ('N/A') to keep the UI clean
      const validMovies = data.Search.filter((movie) => movie.Poster !== 'N/A')
      setMovies(validMovies)
    } else {
      setMovies([])
    }
  }

  // --- FUNCTION 2: GET MOVIE DETAILS ---
  const fetchMovieDetail = async (id) => {
    // Fetch specific details using the 'i' parameter
    const response = await fetch(`https://www.omdbapi.com/?apikey=${API_KEY}&i=${id}`)
    const data = await response.json()
    setSelectedMovie(data) // Opening this state triggers the Modal
  }

  // --- FUNCTION 3: KEYBOARD SUPPORT ---
  const handleKeyDown = (e) => {
    if (e.key === 'Enter') {
      searchMovies(searchTerm)
    }
  }

  // --- RENDER UI ---
  return (
    <div className="app">
      <h1>Simple Movie Search</h1>
      <h3 className="signature">by King Code</h3>

      {/* SEARCH BAR */}
      <div className="search-bar">
        <input
          placeholder="Search for a movie (e.g. Batman)..."
          value={searchTerm} 
          onChange={(e) => setSearchTerm(e.target.value)} 
          onKeyDown={handleKeyDown} 
        />
        <button onClick={() => searchMovies(searchTerm)}>Search</button>
      </div>

      {/* MOVIE GRID SECTION */}
      {movies.length > 0 ? (
        <div className="movie-grid">
          {movies.map((movie) => (
            <div
              className="movie-card"
              key={movie.imdbID} 
              onClick={() => fetchMovieDetail(movie.imdbID)} 
            >
              <img
                src={movie.Poster}
                alt={movie.Title}
                // Hides the card if the image link is broken
                onError={(e) => e.target.parentElement.style.display = 'none'} 
              />
              <h3>{movie.Title}</h3>
              <p>{movie.Year}</p>
            </div>
          ))}
        </div>
      ) : (
        // EMPTY STATE MESSAGING
        <div className="empty">
          {hasSearched ? (
            <h2>No movies found matching "{lastSearch}".</h2>
          ) : (
            <h2>Search for a movie to begin!</h2>
          )}
        </div>
      )}

      {/* MODAL POPUP (Conditionally Rendered) */}
      {selectedMovie && (
        <div className="modal-overlay" onClick={() => setSelectedMovie(null)}>
          {/* stopPropagation prevents clicks inside the box from closing the modal */}
          <div className="modal-content" onClick={(e) => e.stopPropagation()}>

            <button className="close-btn" onClick={() => setSelectedMovie(null)}>Γ—</button>

            <div className="modal-header">
              <img src={selectedMovie.Poster} alt={selectedMovie.Title} />
              <div className="modal-info">
                <h2>{selectedMovie.Title}</h2>
                <p><strong>Released:</strong> {selectedMovie.Released}</p>
                <p><strong>Genre:</strong> {selectedMovie.Genre}</p>
                <p><strong>Director:</strong> {selectedMovie.Director}</p>
                <p><strong>Actors:</strong> {selectedMovie.Actors}</p>
                <p><strong>Rating:</strong> ⭐ {selectedMovie.imdbRating}</p>
                <div className="plot">
                  <p><strong>Plot:</strong></p>
                  <p>{selectedMovie.Plot}</p>
                </div>
              </div>
            </div>
          </div>
        </div>
      )}
    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

🎨 Appendix: The CSS Styles

To make the app look professional, I used a dark theme with a responsive layout.

/* =========================================
   1. GLOBAL RESET & DARK MODE
   Setting a dark background immediately makes the app feel like Netflix/Cinema.
   ========================================= */
body {
  margin: 0;
  padding: 0;
  background-color: #222; /* Dark Grey Background */
  color: white;
  font-family: sans-serif;
  text-align: center;
}

.app {
  max-width: 1200px;
  margin: 0 auto; /* Centers the app container */
  padding: 20px;
}

/* =========================================
   2. THE HEADER (Gradient Text Trick)
   I used a background-clip to create a text gradient.
   It mixes IMDb Yellow (#f5c518) with Netflix Red (#e50914).
   ========================================= */
h1 {
  font-size: 3rem;
  margin-bottom: 10px;
  background: linear-gradient(90deg, #f5c518, #e50914);
  -webkit-background-clip: text; /* Clips the background to the text shape */
  -webkit-text-fill-color: transparent; /* Makes the text transparent so background shows */
}

.signature {
  color: #888;
  margin-top: -10px;
  margin-bottom: 40px;
}

/* =========================================
   3. THE SEARCH BAR
   Using Flexbox here makes it easy to align the input and button.
   ========================================= */
.search-bar {
  display: flex;
  justify-content: center;
  gap: 10px; /* Adds space between input and button */
  margin-bottom: 40px;
}

.search-bar input {
  padding: 15px;
  width: 100%;
  max-width: 400px;
  border-radius: 10px; /* Modern rounded corners */
  border: none;
  background: white;
  color: black;
  font-size: 1rem;
}

.search-bar button {
  padding: 15px 30px;
  border-radius: 10px;
  border: none;
  background-color: #f5c518; /* IMDb Yellow */
  font-weight: bold;
  cursor: pointer;
}

/* =========================================
   4. THE RESPONSIVE GRID SYSTEM
   This is the most powerful line in the CSS.
   It automatically calculates how many columns fit on the screen.
   No Media Queries needed for tablet/desktop resizing!
   ========================================= */
.movie-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 30px;
}

.movie-card {
  background-color: #333;
  border-radius: 10px;
  overflow: hidden;
  transition: transform 0.2s; /* Smooth hover effect */
  cursor: pointer;
}

.movie-card:hover {
  transform: scale(1.05); /* slightly grow the card on hover */
}

.movie-card img {
  width: 100%;
  height: 300px;
  object-fit: cover; /* Ensures image covers the area without stretching */
}

/* =========================================
   5. THE MODAL POPUP
   I used 'position: fixed' to overlay it on top of everything.
   ========================================= */
.modal-overlay {
  position: fixed;
  top: 0; left: 0;
  width: 100%; height: 100%;
  background: rgba(0, 0, 0, 0.85); /* Semi-transparent black */
  display: flex;
  justify-content: center; /* Center horizontally */
  align-items: center;     /* Center vertically */
}

.modal-content {
  background: #222;
  padding: 30px;
  border-radius: 15px;
  max-width: 700px;
  width: 90%;
  text-align: left;
  display: flex; /* Puts image and text side-by-side */
  gap: 20px;
  position: relative; /* Needed for absolute positioning of the Close Button */
}

.modal-content img {
  width: 200px;
  border-radius: 10px;
}

.close-btn {
  position: absolute;
  top: 15px;
  right: 15px; /* Pins button to the top-right corner */
  background: none;
  border: none;
  color: #aaa;
  font-size: 2rem;
  cursor: pointer;
  line-height: 1;
}

/* =========================================
   6. MOBILE OPTIMIZATION
   Small adjustments for phone screens (< 600px).
   ========================================= */
@media (max-width: 600px) {
  /* Stack search bar vertically */
  .search-bar {
    flex-direction: column;
    align-items: center;
  }

  .search-bar input,
  .search-bar button {
    width: 90%; /* full width on mobile */
  }

  /* Stack modal content vertically (Image top, text bottom) */
  .modal-content {
    flex-direction: column;
    align-items: center;
    text-align: center;
  }

  .modal-content img {
    width: 150px; /* Smaller image on mobile */
  }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)