Skip to main content

Overview

GIFs App is a React + TypeScript application that allows users to search and browse GIFs using the Giphy API. The app features search history tracking, intelligent caching with useRef, and a clean, responsive interface.

Giphy Integration

Search and display GIFs using the Giphy API

Smart Caching

Cache search results with useRef for instant loading

Search History

Track up to 7 recent searches with one-click replay

Debounced Search

Automatic search with 700ms debounce delay

Key Features

  • GIF Search: Real-time search powered by Giphy API
  • Debounced Input: Automatic search with configurable delay
  • Search History: Displays up to 7 previous search terms
  • Smart Caching: Uses useRef to cache results and avoid duplicate API calls
  • Instant Replay: Click previous searches to instantly load cached results
  • Axios Integration: Configured Axios instance with interceptors
  • TypeScript: Fully typed interfaces for API responses and data models
  • Testing: Unit tests with Vitest

Project Structure

gifs-app/
├── src/
│   ├── gifs/
│   │   ├── actions/
│   │   │   └── get-gifs-by-query.action.ts  # GIF fetching logic
│   │   ├── api/
│   │   │   └── giphy.api.ts                 # Axios instance
│   │   ├── components/
│   │   │   ├── GifList.tsx                  # GIF grid display
│   │   │   └── PreviousSearches.tsx         # Search history
│   │   ├── hooks/
│   │   │   └── useGifs.tsx                  # Custom GIF hook
│   │   └── interfaces/
│   │       ├── gif.interface.ts             # GIF type
│   │       └── giphy.response.ts            # API response types
│   ├── shared/
│   │   └── components/
│   │       ├── CustomHeader.tsx             # App header
│   │       └── SearchBar.tsx                # Search input
│   └── GifsApp.tsx                          # Main app component
└── package.json

How to Run

1

Set Up Environment

Create a .env file with your Giphy API key:
VITE_GIPHY_API_KEY=your_api_key_here
Get your free API key at Giphy Developers
2

Install Dependencies

npm install
3

Start Development Server

npm run dev
4

Run Tests

npm run test

Main Components

GifsApp Component

The main application component that orchestrates the GIF search functionality.
GifsApp.tsx
import { GifList } from "./gifs/components/GifList";
import { PreviousSearches } from "./gifs/components/PreviousSearches";
import { CustomHeader } from "./shared/components/CustomHeader";
import { SearchBar } from "./shared/components/SearchBar";
import { useGifs } from "./gifs/hooks/useGifs";

export const GifsApp = () => {
  const { handleSearch, handleTermClicked, previousTerms, gifs } = useGifs();
  
  return (
    <>
      <CustomHeader
        title="Buscador de Gifs"
        description="Descubre y comparte el gif"
      />
      
      <SearchBar 
        placeholder="Busca lo que quieras" 
        onQuery={handleSearch} 
      />
      
      <PreviousSearches
        searches={previousTerms}
        onLabelClicked={handleTermClicked}
      />
      
      <GifList gifs={gifs} />
    </>
  );
};

SearchBar Component

A debounced search input that automatically triggers searches after user stops typing.
SearchBar.tsx
import { useEffect, useState, type KeyboardEvent } from "react";

interface Props {
  placeholder?: string;
  onQuery: (query: string) => void;
}

export const SearchBar = ({ placeholder = "Buscar", onQuery }: Props) => {
  const [query, setQuery] = useState("");

  useEffect(() => {
    const timeoutId = setTimeout(() => {
      onQuery(query);
    }, 700);

    return () => {
      clearTimeout(timeoutId);
    };
  }, [query, onQuery]);

  const handleSearch = () => {
    onQuery(query);
  };

  const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
    if (event.key === "Enter") {
      handleSearch();
    }
  };

  return (
    <div className="search-container">
      <input
        type="text"
        placeholder={placeholder}
        value={query}
        onChange={(event) => setQuery(event.target.value)}
        onKeyDown={handleKeyDown}
      />
      <button onClick={handleSearch}>Buscar</button>
    </div>
  );
};
The SearchBar uses a 700ms debounce to prevent excessive API calls while typing.

GifList Component

Displays a grid of GIF results with responsive cards.
GifList.tsx
import type { FC } from "react";
import type { Gif } from "../interfaces/gif.interface";

interface Props {
  gifs: Gif[];
}

export const GifList: FC<Props> = ({ gifs }) => {
  return (
    <div className="gifs-container">
      {gifs.map((gif) => (
        <div key={gif.id} className="gif-card">
          <img src={gif.url} alt={gif.title} />
          <h3>{gif.title}</h3>
          <p>
            {gif.width}x{gif.height}
          </p>
        </div>
      ))}
    </div>
  );
};

Custom Hooks

useGifs Hook

The core hook that manages GIF search, caching, and search history.
useGifs.tsx
import { useRef, useState } from "react";
import { getGifsByQuery } from "../actions/get-gifs-by-query.action";
import type { Gif } from "../interfaces/gif.interface";

export const useGifs = () => {
  const [gifs, setGifs] = useState<Gif[]>([]);
  const [previousTerms, setPreviousTerms] = useState<string[]>([]);

  // Cache using useRef to persist between renders
  const gifsCache = useRef<Record<string, Gif[]>>({});

  const handleTermClicked = async (term: string) => {
    // Check cache first
    if (gifsCache.current[term]) {
      setGifs(gifsCache.current[term]);
      return;
    }
    const gifs = await getGifsByQuery(term);
    setGifs(gifs);
  };

  const handleSearch = async (query: string = "") => {
    query = query.trim().toLowerCase();
    if (query.length === 0) return;

    // Avoid duplicate searches
    if (previousTerms.includes(query)) return;

    // Keep only last 7 searches
    setPreviousTerms([query, ...previousTerms].splice(0, 7));

    const gifs = await getGifsByQuery(query);
    setGifs(gifs);
    
    // Cache the results
    gifsCache.current[query] = gifs;
  };

  return {
    gifs,
    handleSearch,
    previousTerms,
    handleTermClicked,
  };
};
  • Caching with useRef: Results cached in a ref that persists across renders
  • Instant Loading: Cached searches load instantly without API calls
  • History Management: Maintains up to 7 recent search terms
  • Duplicate Prevention: Avoids adding duplicate terms to history
  • Case Normalization: Converts queries to lowercase for consistency

API Integration

Axios Configuration

Configured Axios instance with base URL and default parameters.
giphy.api.ts
import axios from "axios";

export const giphyApi = axios.create({
  baseURL: "https://api.giphy.com/v1/gifs",
  params: {
    lang: "es",
    api_key: import.meta.env.VITE_GIPHY_API_KEY,
  },
});

GIF Fetching Action

Transforms Giphy API response into simplified GIF objects.
get-gifs-by-query.action.ts
import type { GiphyResponse } from "../interfaces/giphy.response";
import type { Gif } from "../interfaces/gif.interface";
import { giphyApi } from "../api/giphy.api";

export const getGifsByQuery = async (query: string): Promise<Gif[]> => {
  const response = await giphyApi<GiphyResponse>("/search", {
    params: {
      q: query,
      limit: 10,
    },
  });
  
  return response.data.data.map((gif) => ({
    id: gif.id,
    title: gif.title,
    url: gif.images.original.url,
    width: Number(gif.images.original.width),
    height: Number(gif.images.original.height),
  }));
};

Type Definitions

export interface Gif {
  id: string;
  title: string;
  url: string;
  width: number;
  height: number;
}

Technologies Used

  • React 19 - UI library
  • TypeScript - Type safety
  • Axios - HTTP client
  • Vite - Build tool with Rolldown
  • Vitest - Testing framework
  • Giphy API - GIF search and retrieval

Performance Optimizations

useRef Caching

Cache search results to avoid duplicate API calls

Debounced Search

Wait 700ms after typing stops before searching

Duplicate Prevention

Check if term already exists before adding to history

Limited Results

Fetch only 10 GIFs per search to reduce load time
This project demonstrates advanced React patterns including useRef for caching, debounced inputs, and API integration with Axios.

Build docs developers (and LLMs) love