Skip to main content

Overview

The GifList component displays an array of GIF objects in a responsive grid layout. Each GIF is shown with its image, title, and dimensions. Location: gifs-app/src/gifs/components/GifList.tsx

Props

gifs
Gif[]
required
Array of GIF objects to display. Each object must match the Gif interface.
interface Gif {
  id: string;
  title: string;
  url: string;
  width: number;
  height: number;
}

Gif Interface

The component expects an array of objects with this structure:
interface Gif {
  id: string;      // Unique identifier (used as React key)
  title: string;   // GIF title/description
  url: string;     // Direct URL to the GIF image
  width: number;   // GIF width in pixels
  height: number;  // GIF height in pixels
}
Source: gifs-app/src/gifs/interfaces/gif.interface.ts:1

Features

  • Grid Layout: Responsive CSS grid using gifs-container class
  • Card Display: Each GIF in a styled gif-card container
  • Metadata Display: Shows title and dimensions for each GIF
  • Key Optimization: Uses unique GIF ID for React list keys
  • TypeScript FC: Uses React.FC type for enhanced type checking
  • Alt Text: Proper alt attributes for accessibility

Usage Example

import { GifList } from './components/GifList';
import type { Gif } from './interfaces/gif.interface';

function GifsApp() {
  const gifs: Gif[] = [
    {
      id: 'abc123',
      title: 'Happy Cat',
      url: 'https://media.giphy.com/media/abc123/giphy.gif',
      width: 480,
      height: 360
    },
    {
      id: 'def456',
      title: 'Dancing Dog',
      url: 'https://media.giphy.com/media/def456/giphy.gif',
      width: 500,
      height: 375
    }
  ];

  return <GifList gifs={gifs} />;
}

Implementation

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} (1.5mb)
          </p>
        </div>
      ))}
    </div>
  );
};

Styling

The component uses two main CSS classes for styling:
.gifs-container {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 1.5rem;
  padding: 2rem;
  max-width: 1400px;
  margin: 0 auto;
}

/* Responsive adjustments */
@media (max-width: 768px) {
  .gifs-container {
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: 1rem;
    padding: 1rem;
  }
}

Data Transformation

Typically, you’ll need to transform API responses to match the Gif interface:
// Example: Transforming Giphy API response
interface GiphyGif {
  id: string;
  title: string;
  images: {
    fixed_height: {
      url: string;
      width: string;
      height: string;
    };
  };
}

function transformGiphyData(giphyGifs: GiphyGif[]): Gif[] {
  return giphyGifs.map(gif => ({
    id: gif.id,
    title: gif.title || 'Untitled',
    url: gif.images.fixed_height.url,
    width: parseInt(gif.images.fixed_height.width),
    height: parseInt(gif.images.fixed_height.height)
  }));
}

// Usage in a hook
export function useGifs() {
  const [gifs, setGifs] = useState<Gif[]>([]);

  const searchGifs = async (query: string) => {
    const response = await fetch(`https://api.giphy.com/v1/gifs/search?q=${query}`);
    const data = await response.json();
    const transformedGifs = transformGiphyData(data.data);
    setGifs(transformedGifs);
  };

  return { gifs, searchGifs };
}

Performance Optimization

Implement lazy loading for better performance with many GIFs:
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}
            loading="lazy" // Native lazy loading
          />
          <h3>{gif.title}</h3>
          <p>{gif.width}x{gif.height}</p>
        </div>
      ))}
    </div>
  );
};
For very large lists, use react-window or react-virtualized:
import { FixedSizeGrid } from 'react-window';

const GifCell = ({ columnIndex, rowIndex, style, data }) => {
  const index = rowIndex * 4 + columnIndex;
  const gif = data.gifs[index];
  if (!gif) return null;

  return (
    <div style={style} className="gif-card">
      <img src={gif.url} alt={gif.title} />
      <h3>{gif.title}</h3>
    </div>
  );
};

export const VirtualizedGifList: FC<Props> = ({ gifs }) => {
  return (
    <FixedSizeGrid
      columnCount={4}
      columnWidth={250}
      height={600}
      rowCount={Math.ceil(gifs.length / 4)}
      rowHeight={300}
      width={1000}
      itemData={{ gifs }}
    >
      {GifCell}
    </FixedSizeGrid>
  );
};
Prevent unnecessary re-renders with React.memo:
import { memo } from 'react';

export const GifList: FC<Props> = memo(({ gifs }) => {
  return (
    <div className="gifs-container">
      {gifs.map((gif) => (
        <GifCard key={gif.id} gif={gif} />
      ))}
    </div>
  );
});

const GifCard = memo(({ gif }: { gif: Gif }) => (
  <div className="gif-card">
    <img src={gif.url} alt={gif.title} />
    <h3>{gif.title}</h3>
    <p>{gif.width}x{gif.height}</p>
  </div>
));

Enhanced Features

interface Props {
  gifs: Gif[];
  onGifClick?: (gif: Gif) => void;
}

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

Testing

import { render, screen } from '@testing-library/react';
import { GifList } from './GifList';
import type { Gif } from '../interfaces/gif.interface';

const mockGifs: Gif[] = [
  {
    id: '1',
    title: 'Test GIF 1',
    url: 'https://example.com/gif1.gif',
    width: 480,
    height: 360
  },
  {
    id: '2',
    title: 'Test GIF 2',
    url: 'https://example.com/gif2.gif',
    width: 500,
    height: 375
  }
];

describe('GifList', () => {
  it('renders all gifs', () => {
    render(<GifList gifs={mockGifs} />);
    expect(screen.getByText('Test GIF 1')).toBeInTheDocument();
    expect(screen.getByText('Test GIF 2')).toBeInTheDocument();
  });

  it('displays correct dimensions', () => {
    render(<GifList gifs={mockGifs} />);
    expect(screen.getByText('480x360 (1.5mb)')).toBeInTheDocument();
  });

  it('renders images with correct alt text', () => {
    render(<GifList gifs={mockGifs} />);
    const img = screen.getByAlt('Test GIF 1');
    expect(img).toHaveAttribute('src', 'https://example.com/gif1.gif');
  });

  it('renders empty when no gifs provided', () => {
    const { container } = render(<GifList gifs={[]} />);
    expect(container.querySelector('.gif-card')).not.toBeInTheDocument();
  });
});

SearchBar

Search component that populates GifList

PreviousSearches

Shows clickable list of past searches

CustomHeader

Page header above the GIF grid

Best Practices

Always use unique id values for React keys
Provide meaningful alt text for accessibility
Implement lazy loading for images
Handle empty state gracefully
Use responsive grid layouts
Consider virtualization for large lists
Add error handling for failed image loads
Optimize image sizes for web performance

Build docs developers (and LLMs) love