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
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
Basic Usage
With API Data
Empty State
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 } /> ;
}
import { GifList } from './components/GifList' ;
import { useGifs } from './hooks/useGifs' ;
function GifsPage () {
const { gifs , isLoading } = useGifs ();
if ( isLoading ) return < p > Cargando... </ p > ;
return (
< div >
< h2 > Resultados de búsqueda </ h2 >
< GifList gifs = { gifs } />
</ div >
);
}
import { GifList } from './components/GifList' ;
function GifsPage ({ gifs } : { gifs : Gif [] }) {
if ( gifs . length === 0 ) {
return (
< div className = "empty-state" >
< p > No se encontraron GIFs </ p >
</ div >
);
}
return < GifList gifs = { gifs } /> ;
}
Implementation
Component Source
Gif Interface
Complete Example
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:
Grid Container
GIF Card
Alternative Masonry Layout
.gifs-container {
display : grid ;
grid-template-columns : repeat ( auto-fill , minmax ( 250 px , 1 fr ));
gap : 1.5 rem ;
padding : 2 rem ;
max-width : 1400 px ;
margin : 0 auto ;
}
/* Responsive adjustments */
@media ( max-width : 768 px ) {
.gifs-container {
grid-template-columns : repeat ( auto-fill , minmax ( 150 px , 1 fr ));
gap : 1 rem ;
padding : 1 rem ;
}
}
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 };
}
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
Click Handler
Loading State
Error Boundaries
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 >
);
};
interface Props {
gifs : Gif [];
isLoading ?: boolean ;
}
export const GifList : FC < Props > = ({ gifs , isLoading }) => {
if ( isLoading ) {
return (
< div className = "gifs-container" >
{ Array . from ({ length: 12 }). map (( _ , i ) => (
< div key = { i } className = "gif-card skeleton" >
< div className = "skeleton-image" />
< div className = "skeleton-text" />
</ div >
)) }
</ div >
);
}
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 >
);
};
const GifCard = ({ gif } : { gif : Gif }) => {
const [ error , setError ] = useState ( false );
return (
< div className = "gif-card" >
{ error ? (
< div className = "gif-error" >
< span > ❌ </ span >
< p > Error loading GIF </ p >
</ div >
) : (
< img
src = { gif . url }
alt = { gif . title }
onError = { () => setError ( true ) }
/>
) }
< h3 > { gif . title } </ h3 >
< p > { gif . width } x { gif . height } </ p >
</ div >
);
};
export const GifList : FC < Props > = ({ gifs }) => {
return (
< div className = "gifs-container" >
{ gifs . map (( gif ) => (
< GifCard key = { gif . id } gif = { gif } />
)) }
</ 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