Skip to main content

Overview

The useGifs hook provides a complete solution for searching and displaying GIFs from the Giphy API. It includes client-side caching, search history tracking, and optimized re-rendering through the use of useRef for cache storage.

Purpose

This hook demonstrates intermediate to advanced patterns for:
  • Managing async API calls in custom hooks
  • Implementing client-side caching with useRef
  • Tracking and displaying search history
  • Handling multiple pieces of related state
  • Avoiding unnecessary API calls with cache lookups

API Reference

Hook Signature

useGifs(): UseGifsReturn

Parameters

This hook takes no parameters.

Return Value

The hook returns an object with the following properties:
gifs
Gif[]
required
Array of GIF objects returned from the current or cached search. Each Gif object contains:
  • id: string - Unique identifier for the GIF
  • title: string - Title or description of the GIF
  • url: string - URL to the GIF image
  • width: number - Width in pixels
  • height: number - Height in pixels
Async function to search for GIFs. Validates the query, checks the cache, updates search history, and fetches from the API if needed.
previousTerms
string[]
required
Array of up to 7 most recent search terms, ordered from newest to oldest.
handleTermClicked
(term: string) => Promise<void>
required
Async function to load GIFs from a previous search term. Always uses cached data if available, otherwise fetches from the API.

Implementation Details

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[]>([]);

  const gifsCache = useRef<Record<string, Gif[]>>({});

  const handleTermClicked = async (term: string) => {
    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; // validate not empty

    if (previousTerms.includes(query)) return;

    setPreviousTerms([query, ...previousTerms].splice(0, 7));

    const gifs = await getGifsByQuery(query);

    setGifs(gifs);
    gifsCache.current[query] = gifs;
  };

  return {
    gifs,
    handleSearch,
    previousTerms,
    handleTermClicked,
  };
};

Key Implementation Details

The hook uses useRef to store a cache object (gifsCache.current) that persists across re-renders without causing them. This is more efficient than using useState for cache storage, as updates to the ref don’t trigger component re-renders.
const gifsCache = useRef<Record<string, Gif[]>>({});
Search queries are normalized by trimming whitespace and converting to lowercase:
query = query.trim().toLowerCase();
This ensures consistent cache lookups and prevents duplicate entries with different casing.
The hook maintains up to 7 most recent search terms:
setPreviousTerms([query, ...previousTerms].splice(0, 7));
New terms are added to the front, and the array is limited to 7 items using splice(0, 7).
The hook prevents duplicate searches by checking if a term already exists in previousTerms:
if (previousTerms.includes(query)) return;
When clicking a previous search term, the hook checks the cache first:
if (gifsCache.current[term]) {
  setGifs(gifsCache.current[term]);
  return;
}
This provides instant results without making unnecessary API calls.

Usage Examples

Basic Usage

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} />
    </>
  );
};

Advanced Usage with Loading States

GifSearchWithLoading.tsx
import { useState } from "react";
import { useGifs } from "./hooks/useGifs";

export const GifSearchWithLoading = () => {
  const { gifs, handleSearch, previousTerms, handleTermClicked } = useGifs();
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const handleSearchWithLoading = async (query: string) => {
    setIsLoading(true);
    setError(null);
    
    try {
      await handleSearch(query);
    } catch (err) {
      setError(err instanceof Error ? err.message : "Failed to fetch GIFs");
    } finally {
      setIsLoading(false);
    }
  };

  const handleTermClickWithLoading = async (term: string) => {
    setIsLoading(true);
    setError(null);
    
    try {
      await handleTermClicked(term);
    } catch (err) {
      setError(err instanceof Error ? err.message : "Failed to load GIFs");
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div>
      <SearchBar onQuery={handleSearchWithLoading} />
      
      {error && <div className="error">{error}</div>}
      
      <PreviousSearches
        searches={previousTerms}
        onLabelClicked={handleTermClickWithLoading}
      />
      
      {isLoading ? (
        <div>Loading GIFs...</div>
      ) : (
        <GifList gifs={gifs} />
      )}
    </div>
  );
};

Design Patterns

This hook demonstrates several important React patterns:

1. Separation of Concerns

The hook separates data fetching logic from the API action:
  • useGifs manages state and caching
  • getGifsByQuery handles the API call and data transformation

2. useRef for Non-Rendering State

Using useRef for the cache prevents unnecessary re-renders:
// ✅ Efficient: Doesn't cause re-renders
const gifsCache = useRef<Record<string, Gif[]>>({});

// ❌ Less efficient: Would cause re-render on every cache update
const [gifsCache, setGifsCache] = useState<Record<string, Gif[]>>({});

3. Optimistic UI Updates

Cached results are displayed immediately without loading states:
if (gifsCache.current[term]) {
  setGifs(gifsCache.current[term]); // Instant update
  return;
}

4. Data Normalization

Query normalization ensures consistent behavior:
query = query.trim().toLowerCase();

Performance Considerations

The useRef cache persists for the component’s lifetime, reducing API calls significantly. Once a search is performed, subsequent clicks on that term are instant.
The current implementation stores all search results indefinitely. For production, consider:
  • Adding cache size limits
  • Implementing LRU (Least Recently Used) eviction
  • Clearing cache on unmount
The 7-term limit prevents the history from growing unbounded while providing enough context for recent searches.

Potential Improvements

Consider these enhancements for production use:
  1. Loading States: Add loading indicators for better UX
  2. Error Handling: Add try-catch blocks and error states
  3. Debouncing: Implement search debouncing to reduce API calls
  4. Cache Persistence: Store cache in localStorage for cross-session persistence
  5. Cache Expiration: Add timestamps and expiration logic
  6. TypeScript Return Type: Define explicit return type interface
interface UseGifsReturn {
  gifs: Gif[];
  handleSearch: (query: string) => Promise<void>;
  previousTerms: string[];
  handleTermClicked: (term: string) => Promise<void>;
  isLoading: boolean;
  error: string | null;
}

useCounter

Learn about basic custom hook patterns

useWeather

Explore more complex async patterns with useEffect

Build docs developers (and LLMs) love