42. 8 Agregando keys.

 

Tutorial 42.8: Agregando Keys a los componentes React

Solucionando advertencias de React sobre Keys

En este tutorial vamos a solucionar una advertencia importante que React nos muestra en la consola cuando renderizamos listas de componentes.

El problema: Listas sin keys

Cuando abres la consola del navegador, probablemente ves este error:

text

Warning: Each child in a list should have a unique "key" prop.

Esta advertencia aparece porque React necesita una forma eficiente de identificar qué elementos han cambiado, se han agregado o se han eliminado cuando renderiza listas.

Paso 1: Identificar el problema

En nuestro ContextMovieCard.jsx, tenemos este código:

jsx

{movies.map((movie) => (

    <MovieCard 

        movie={movie} 

    />

))}

Problema: Cada MovieCard no tiene una key única.

Paso 2: Por qué necesitamos keys

React usa las keys para:

  1. Identificar elementos únicos en una lista

  2. Optimizar el rendimiento al actualizar solo los elementos que cambiaron

  3. Mantener el estado de los componentes cuando la lista se reordena

  4. Evitar bugs en la interfaz de usuario

Paso 3: Encontrar el identificador único

En la consola, cuando inspeccionamos los datos de la API, vemos que cada película tiene un id único:

javascript

{

    "id": 12345,

    "title": "Nombre de la Película",

    "poster_path": "/imagen.jpg",

    // ... más propiedades

}

El id es perfecto para usar como key porque:

  • Es único para cada película

  • No cambia

  • Viene directamente de la API

Paso 4: Solucionar el problema en ContextMovieCard

ContextMovieCard.jsx corregido

jsx

import { useEffect, useState } from "react";

import { get } from "../data/httpClient";

import { MovieCard } from "../components/MovieCard";

import "./ContextMovieCard.css";


export function ContextMovieCard() {

    const [movies, setMovies] = useState([]);

    const [loading, setLoading] = useState(true);


    useEffect(() => {

        get("/discover/movie").then((data) => {

            setMovies(data.results);

            console.log("Películas obtenidas:", data);

            setLoading(false);

        });

    }, []);


    if (loading) {

        return (

            <div className="loading">

                <p>Cargando películas...</p>

            </div>

        );

    }


    return (

        <div className="container">

            <ul className="movies-grid">

                {movies.map((movie) => (

                    <MovieCard 

                        key={movie.id}  // ¡KEY AGREGADA AQUÍ!

                        movie={movie} 

                    />

                ))}

            </ul>

        </div>

    );

}

Paso 5: Mejorando ContextMovieCard.css con margen

ContextMovieCard.css actualizado

css

/* Contenedor principal */

.container {

    padding: 40px;

    max-width: 1400px;

    margin: 0 auto;

    margin-top: 20px; /* Margen superior agregado */

}


/* Grid de películas */

.movies-grid {

    display: grid;

    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));

    gap: 30px;

    justify-content: center;

    padding: 0;

    margin: 0;

    list-style: none;

}


/* Estado de carga */

.loading {

    display: flex;

    justify-content: center;

    align-items: center;

    height: 300px;

    font-size: 1.2rem;

    color: #ffd700;

    background: rgba(106, 13, 173, 0.1);

    border-radius: 10px;

    margin: 40px;

    margin-top: 20px; /* Margen superior agregado */

}


.loading::before {

    content: "";

    width: 40px;

    height: 40px;

    border: 4px solid rgba(255, 215, 0, 0.3);

    border-top: 4px solid #ffd700;

    border-radius: 50%;

    margin-right: 15px;

    animation: spin 1s linear infinite;

}


@keyframes spin {

    0% { transform: rotate(0deg); }

    100% { transform: rotate(360deg); }

}


/* Título de la sección */

.section-title {

    text-align: center;

    color: #ffffff;

    font-size: 2rem;

    margin-top: 20px; /* Margen superior agregado */

    margin-bottom: 30px;

    font-weight: 600;

}


/* Responsive */

@media (max-width: 768px) {

    .container {

        padding: 20px;

        margin-top: 15px;

    }

    

    .movies-grid {

        grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));

        gap: 20px;

    }

    

    .section-title {

        margin-top: 15px;

        font-size: 1.6rem;

    }

}


@media (max-width: 560px) {

    .movies-grid {

        grid-template-columns: 1fr;

        max-width: 100%;

    }

    

    .container {

        padding: 15px;

        margin-top: 10px;

    }

    

    .section-title {

        margin-top: 10px;

        font-size: 1.4rem;

    }

}

Paso 6: Actualizar MovieCard.jsx

MovieCard.jsx optimizado

jsx

import "./MovieCard.css";


export function MovieCard({ movie }) {

    // Construir URL de la imagen

    const imageUrl = movie.poster_path 

        ? `https://image.tmdb.org/t/p/w300${movie.poster_path}`

        : "";


    return (

        <li className="movie-card">

            <img

                src={imageUrl}

                alt={movie.title}

                className="movie-image"

                width="230"

                height="345"

                loading="lazy" // Optimización de carga

            />

            <div className="movie-info">

                <h3 className="movie-title">{movie.title}</h3>

                <div className="movie-details">

                    <span className="rating">

                        ⭐ {movie.vote_average.toFixed(1)}

                    </span>

                    <span className="year">

                        {new Date(movie.release_date).getFullYear()}

                    </span>

                </div>

            </div>

        </li>

    );

}

Paso 7: Mejorar MovieCard.css

MovieCard.css actualizado

css

/* Reset de estilos para lista */

.movie-card {

    list-style: none;

    margin: 0;

    padding: 0;

    font-size: 1rem;

    text-align: center;

    transition: all 0.5s ease;

    border-radius: 15px;

    overflow: hidden;

    background: linear-gradient(135deg, #16213e 0%, #0f3460 100%);

    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);

    position: relative;

    cursor: pointer;

    height: 100%;

    display: flex;

    flex-direction: column;

}


/* Efecto hover */

.movie-card:hover {

    transform: translateY(-10px);

    box-shadow: 0 20px 40px rgba(255, 215, 0, 0.2);

    z-index: 10;

}


.movie-card:hover .movie-image {

    transform: scale(1.05);

}


/* Contenedor de la imagen */

.movie-image {

    width: 100%;

    height: 345px;

    object-fit: cover;

    display: block;

    transition: transform 0.5s ease;

    background: linear-gradient(45deg, #1a1a2e, #16213e);

}


/* Información de la película */

.movie-info {

    padding: 15px 10px;

    background: rgba(22, 33, 62, 0.95);

    backdrop-filter: blur(10px);

    border-top: 2px solid rgba(255, 215, 0, 0.3);

    flex-grow: 1;

    display: flex;

    flex-direction: column;

    justify-content: space-between;

}


.movie-title {

    margin: 0 0 10px 0;

    color: #ffffff;

    font-size: 1rem;

    font-weight: 600;

    line-height: 1.3;

    white-space: nowrap;

    overflow: hidden;

    text-overflow: ellipsis;

}


.movie-details {

    display: flex;

    justify-content: space-between;

    align-items: center;

    font-size: 0.9rem;

}


.rating {

    color: #ffd700;

    font-weight: bold;

    display: flex;

    align-items: center;

    gap: 5px;

}


.year {

    color: #a0a0a0;

}


/* Responsive */

@media (max-width: 768px) {

    .movie-card {

        font-size: 0.9rem;

    }

    

    .movie-image {

        height: 300px;

    }

    

    .movie-info {

        padding: 12px 8px;

    }

    

    .movie-title {

        font-size: 0.9rem;

        margin-bottom: 8px;

    }

    

    .movie-details {

        font-size: 0.8rem;

    }

}


@media (max-width: 560px) {

    .movie-card {

        max-width: 100%;

    }

    

    .movie-card:hover {

        transform: translateY(-5px);

    }

}

Paso 8: Actualizar App.jsx con mejoras

App.jsx final

jsx

import './App.css';

import MyRoutes from './routers/routes';


function App() {

    return (

        <div className="App">

            <header>

                <h1 className='title'>Películas 🎬</h1>

                <p className="subtitle">Descubre las mejores películas del momento</p>

            </header>

            <main>

                <MyRoutes />

            </main>

            <footer className="app-footer">

                <p>Datos proporcionados por TMDb API</p>

            </footer>

        </div>

    );

}


export default App;

Paso 9: Agregar estilos al footer en App.css

App.css actualizado

css

/* ... estilos anteriores ... */


.subtitle {

    text-align: center;

    color: #b0b0b0;

    font-size: 1.1rem;

    margin-top: 5px;

    font-weight: 400;

}


.app-footer {

    text-align: center;

    padding: 1rem;

    background: rgba(22, 33, 62, 0.8);

    border-top: 1px solid rgba(255, 215, 0, 0.2);

    margin-top: auto;

    font-size: 0.85rem;

    color: #a0a0a0;

}


/* Responsive para footer */

@media (max-width: 768px) {

    .subtitle {

        font-size: 1rem;

    }

    

    .app-footer {

        padding: 0.8rem;

        font-size: 0.8rem;

    }

}

¿Qué hemos logrado?

  1. ✅ Eliminamos la advertencia de keys en la consola

  2. ✅ Mejoramos el rendimiento de React al identificar elementos únicos

  3. ✅ Agregamos márgenes para mejor espaciado visual

  4. ✅ Mantenemos el estado correcto de los componentes

  5. ✅ Prevenimos bugs potenciales en la interfaz

Diferencia entre advertencias y errores

Es importante distinguir:

  • Advertencias (Warnings): El código funciona, pero podría ser más eficiente

  • Errores (Errors): El código no funciona y necesita corrección

En nuestro caso, la advertencia de keys era importante corregir para:

  1. Mejorar el rendimiento

  2. Evitar comportamientos inesperados

  3. Seguir las mejores prácticas de React

Resultado final

Ahora tu aplicación:

  • ✅ No muestra advertencias en la consola

  • ✅ Tiene mejor espaciado visual con márgenes

  • ✅ Es más eficiente en el renderizado

  • ✅ Sigue las mejores prácticas de React

  • ✅ Está lista para producción

¡Felicidades! Tu aplicación de películas ahora está optimizada y lista para el siguiente nivel


Comentarios

Entradas más populares de este blog

11. Creando un proyecto react con create-react-app

29. useEffect

38. 4 Routers