theme-sticky-logo-alt
Como Reemplazar Imágenes Rotas en React colocando una imagen por defecto - fallback images on error

Como Reemplazar Imágenes Rotas en React colocando una imagen por defecto (placeholder) – fallback images on error

Cuando iniciamos en el mundo de React solemos encontrarnos con algunos problemas a tareas comunes, como lo es el manejo de imágenes, ya que por ejemplo podemos mandar a llamar datos de una API, donde en alguno de los atributos venga la URL de la imagen que queremos mostrar, pero al intentar mostrarla resulta que se muestra como una imagen rota en el navegador, esto puede tener diferentes causas y soluciones, que es precisamente lo que analizaremos en este artículo.

Proyecto: Recetario de Cocina

Para realizar la demostración del problema de las imagenes rotas en React, vamos a realizar un sencillo ejemplo de una Aplicación que muestre recetas de cocinas, estas estarán contenidas dentro de un JSON preparado especialmente diferentes errores para poder entender mejor la problematica.

Para este proyecto use Vite y el repositorio lo pueden encontrar en la siguiente liga: https://github.com/warderer/react-image-default donde pueden recorrer el historial de commits para ver desde el problema inicial hasta las soluciones paso a paso.

Setup Inicial del Proyecto

Para crear este proyecto, es necesario tengas instalado Node.js. Utilizaremos Vite para la creación de nuestro proyecto y lo inicializaremos de la siguiente forma:

npm init vite@latest react-recipes -- --template react

A continuación entraremos en la carpeta del proyecto:

cd react-recipes

Una vez dentro de la carpeta del proyecto, instalaremos las dependencias iniciales, ejecutando en la terminal:

npm install

Instalación de StandardJS y config de Linter

Instalar como dependencia de desarrollo la librería de Standard JS que contiene una configuración para ESLint que podemos usar:

npm i standard -D

Ahora, para poder usar la configuración de ESLint anteriormente instalada, añadimos la configuración de ESLint en el archivo package.json

package.json

  "eslintConfig": {
    "extends": [
      "./node_modules/standard/eslintrc.json"
    ]
  }

Si quieres ver un poco más de detalle sobre la configuración de ESLint en este otro artículo puedes encontrar más información al respecto.

Archivo de Recetas de Cocina: recipes.json

Creamos un archivo en la siguiente ruta: /src/assets/recipes.json que contendrá un arreglo con las recetas de cocina (para simular la data que nos podría traer una API):

/src/assets/recipes.json

[
  {
    "id": 1,
    "title": "Chilaquiles",
    "cover": "https://images.unsplash.com/photo-1640719028782-8230f1bdc42a?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NHx8Y2hpbGFxdWlsZXN8ZW58MHx8MHx8&auto=format&fit=crop&w=500&q=60"
  },
  {
    "id": 2,
    "title": "Quesadillas con Queso",
    "cover": "https://images.unsplash.com/photo-122618040996337-56904b7850b9?ixlib=rb-4.0.3&ixid=sMnwxMjA3fDB8MHxzZWFyY2h8M3x8cXVlc2FkaWxsYXN8ZW58MHx8MHx8&auto=format&fit=crop&w=500&q=60"
  },
  {
    "id": 3,
    "title": "Tamales",
    "cover": "https://images.unsplash.com/photo-1629741884398-68d755d34404?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Nnx8dGFtYWx8ZW58MHx8MHx8&auto=format&fit=crop&w=500&q=60"
  },
  {
    "id": 4,
    "title": "Pozole"
  },
  {
    "id": 5,
    "title": "Panuchos",
    "cover": ""
  },
  {
    "id": 6,
    "title": "Cochinita Pibil",
    "cover": "http://"
  }
]

En el archivo JSON es importante destacar lo siguiente:

  • Chilaquiles: Contiene un atributo cover con una URL correcta. Por lo que esta imagen debería mostrarse sin problemas.
  • Quesadillas con Queso: Contiene un atributo cover con una URL incorrecta. Contiene caracteres de más por lo cual la URL simplemente no existe y devolverá error 404 (not found), por lo tanto no debería mostrarse.
  • Tamales: Contiene un atributo cover con una URL correcta. Por lo que esta imagen debería mostrarse sin problemas.
  • Pozole: No contiene un atributo cover. Por lo tanto no esta en condiciones de renderizar una imagen, ya que al intentarlo marcará la imagen como rota.
  • Panuchos: Contiene un atributo cover, sin embargo este viene con un string vacío, por lo tanto se renderizará como una imagen rota.
  • Cochinita Pibil: Contiene un atributo cover, y es similar al caso de las Quesadillas con Queso, por lo tanto al ser una URL incorrecta el navegador la renderizará como una imagen rota.

Se modifico el contenido de App.css de Vite, reemplazándolo por el siguiente código CSS, simplemente para poder colocar varios elementos en una fila:

/src/App.css

.recipes {
    display: flex;
    align-items: center;
    flex-direction: column;
    width: 100%;
    font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
    font-size: 16px;
    line-height: 24px;
    font-weight: 400;
}

  .recipes__list {
    display: grid;
    justify-items: center;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    grid-template-rows: 1fr;
    grid-column-gap: 30px;
    grid-row-gap: 40px;
    width: 100%;
    max-width: 960px;
  }

  .recipes__item {
    display:flex;
    justify-content: center;
    flex-direction: column;
    max-width: 300px;
  }

Renderizando el contenido de nuestras recetas

Ahora que tenemos todo los archivos del proyecto en orden, procederemos a modificar el componente App para agregar los estilos de App.css y que nos muestre las recetas de cocinas del archivo recipes.json como es usual en React, usaremos un Map para recorrer e imprimir dicho arreglo, y tomando los elementos id, title y cover del mismo para colocar su imagen y titulo correspondiente.

/src/App.jsx

import './App.css'
import recipes from './assets/recipes.json'

function App () {
  return (
    <div className='recipes'>
      <h2 className='recipes__main-title'>Recetas de Cocina</h2>
      <div className='recipes__list'>
        {recipes.map(({ id, title, cover }) => (
          <div className='recipes__item' key={id}>
            <img src={cover} alt={title} />
            <h4>{title}</h4>
          </div>
        ))}
      </div>
    </div>
  )
}

export default App

Ahora procederemos a ver el resultado de nuestro proyecto usando el comando:

npm run dev
Algunas imágenes en React aparecen rotas

Anteriormente habíamos analizado los posibles detalles con los que vienen los archivos de imagen en el archivo JSON. Observando como viene la información y la naturaleza de las URL, tenemos dos posibles escenarios de problemas con las imágenes.

Escenario 1: El atributo con la información de la imagen no existe o este viene vacío.

Notamos que en JSON existen elementos que no contienen el atributo referente a la imagen (el caso de Pozole), o bien este existe pero viene vacío (el caso los Panuchos).

Primero que nada declaramos una constante placeholderImage que contenga la URL de la imagen que usaremos por defecto. Es decir, es la imagen que decidiremos usar en lugar de la imagen “original” debido a que esta presenta un problema, como el anteriormente mencionado.

En esta primera aproximación validaremos que el atributo exista o bien que este tenga algún valor diferente a vacío. Por medio de un operador lógico OR ( || ) podemos asignar un valor por defecto en caso de que el valor de cover se evalué como falso, recordemos que en JavaScript a esto se les conoce como falsy : al evaluar valores como 0, “”, null, undefined, NaN estos devolveran false.

/src/App.jsx

import './App.css'
import recipes from './assets/recipes.json'

function App () {
  const placeholderImage = 
  'https://images.unsplash.com/photo-1542010589005-d1eacc3918f2?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8cmVjaXBlfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=500&q=60'

  return (
    <div className='recipes'>
      <h2 className='recipes__main-title'>Recetas de Cocina</h2>
      <div className='recipes__list'>
        {recipes.map(({ id, title, cover }) => (
          <div className='recipes__item' key={id}>
            <img src={cover || placeholderImage} alt={title} />
            <h4>{title}</h4>
          </div>
        ))}
      </div>
    </div>
  )
}

Nuevamente al correr la aplicación con estos cambios (npm run dev), notaremos un cambio: ¡Ahora Pozole y Panuchos usaran la imagen por defecto en vez de mostrar la imagen como rota!

Se reparan las imágenes que no tienen el atributo correcto o bien no tiene valor definido

¿Pero qué pasa con las otras dos imágenes: Quesadillas con queso y Cochinita Pibil?

Escenario 2: El atributo si viene con una URL en el atributo cover para la imagen, pero esta devuelve error 404 por ser incorrecta o ya no existir.

En el primer escenario validamos que el atributo exista o que este no venga vacío, sin embargo ¿Qué pasa cuando si tiene una URL, pero resulta que esta no es válida o bien la imagen ya no existe en dicha dirección devolviendo un error 404 (Not found)?

Para resolver este detalle, primero necesito platicarles de la existencia del atributo onError de la etiqueta img. Resulta que onError es un evento que se dispara cuando un error ocurre al cargar un archivo externo (puede ser una imagen o archivo), que es precisamente lo que ocurre en el caso de Quesadillas con Queso y Cochinita Pibil, que contienen URLs inválidas o que ya no funcionan.

/src/App.jsx

import './App.css'
import recipes from './assets/recipes.json'

function App () {
  const placeholderImage =
  'https://images.unsplash.com/photo-1542010589005-d1eacc3918f2?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8cmVjaXBlfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=500&q=60'

  const handleImageError = (e) => {
    e.target.src = placeholderImage
  }

  return (
    <div className='recipes'>
      <h2 className='recipes__main-title'>Recetas de Cocina</h2>
      <div className='recipes__list'>
        {recipes.map(({ id, title, cover }) => (
          <div className='recipes__item' key={id}>
            <img
              src={cover || placeholderImage}
              alt={title}
              onError={handleImageError}
            />
            <h4>{title}</h4>
          </div>
        ))}
      </div>
    </div>
  )
}

export default App

En el código anterior, agregamos entonces el atributo onError a nuestra etiqueta de img, si se da el caso de que no cargue el archivo automáticamente va a disparar la función declara con anterioridad handleImageError, esta función recibe el evento e que lo disparo como parámetro, esto es importante por que necesita saber que imagen es la que tuvo el problema, de esta forma mediante el atributo target.src reemplazamos la imagen colocada en el src original por nuestra imagen por defecto placeholderImage.

Y obtenemos el siguiente resultado:

Ahora en React, las imágenes rotas muestran una imagen por defecto

Plus: Creando nuestro componente de Imagen en React con soporte para manejar una imagen por defecto

Si bien la aproximación anterior de usar e.target.src en la función handleImageError funciona. Tenemos que tener en cuenta que esta haciendo una manipulación directa del DOM, por lo que React en realidad no esta enterado de este cambio.

Una mejor forma de resolver esto al estilo React, es creando nuestro propio componente de imagen, que reciba como props todos los atributos que pueda tener una etiqueta img de HTML (src, alt, height, width, etc.), y como plus que se le pueda indicar una imagen por defecto, donde además se utilizará un estado para contener el valor src de la imagen, este estado se actualizará en caso de ser necesario, reemplazando la imagen original por la imagen por defecto, de esta forma, React tendrá conocimiento y control sobre lo que sucede con la imagen.

Así pues creando un nuevo archivo para crear nuestro ImageComponent, quedaría de la siguiente forma:

/src/components/ImageComponent.jsx

import { useState } from 'react'

const ImageComponent = (props) => {
  const { notFoundSrc, src, ...imageAttributes } = props
  const [imgSrc, setImgSrc] = useState(src)

  return (
    <img
      {...imageAttributes}
      src={imgSrc || notFoundSrc}
      onError={() => { setImgSrc(notFoundSrc) }}
    />
  )
}
export default ImageComponent

Ahora explicaremos en que consiste cada línea de código de este componente:

const { notFoundSrc, src, ...imageAttributes } = props

Separamos notFoundSrc y src del resto de los atributos de la etiqueta img. Esto debido a que más adelante inyectaremos en la etiqueta de imagen los atributos y si colocamos un atributo como notFoundSrc nos marcará error. Y src lo dejamos para manipularlo de forma independiente en un estado más fácil.

const [imgSrc, setImgSrc] = useState(src)

Generamos un estado que contendra la imagen a mostrar, por defecto es la imagen original para que de esta forma React se entere del cambio que sufre la etiqueta img.

{...imageAttributes}

imageAttributes contiene los atributos adicionales de img: alt, height, width, etc., en caso de que vengan como props. Entonces los colocamos dentro de la etiqueta de imagen para que coloque estos valores (en caso de que existan).

src={imgSrc || notFoundSrc} 

Validamos que exista el atributo o bien el valor de la URL de la imagen (Caso #1).

onError={() => { setImgSrc(notFoundSrc) }}

Si se dispará el evento onError, entonces cambiamos la imagen original por la imagen de error (Caso #2), como usamos setImgSrc para modificar el estado, esto ocasionará que se renderice nuevamente la imagen con el nuevo valor, que en este caso será la imagen por defecto notFound que le enviamos como prop al componente.

Ahora refactorizando un poco el código de App.jsx para hacer uso de ImageComponent quedaría de la siguiente forma:

Ahora refactorizando un poco el código de App.jsx para hacer uso de ImageComponent quedaría de la siguiente forma:

/src/App.jsx

import './App.css'
import recipes from './assets/recipes.json'
import ImageComponent from './components/ImageComponent'

function App () {
  const placeholderImage =
  'https://images.unsplash.com/photo-1542010589005-d1eacc3918f2?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8cmVjaXBlfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=500&q=60'

  return (
    <div className='recipes'>
      <h2 className='recipes__main-title'>Recetas de Cocina</h2>
      <div className='recipes__list'>
        {recipes.map(({ id, title, cover }) => (
          <div className='recipes__item' key={id}>
            <ImageComponent
              src={cover}
              alt={title}
              notFoundSrc={placeholderImage}
            />
            <h4>{title}</h4>
          </div>
        ))}
      </div>
    </div>
  )
}

export default App

Como pueden ver es una aproximación más al estilo de React, muy limpia y reutilizable en varios proyectos. Espero les sea útil y cualquier propuesta de mejora es bienvenida.

Referencias

Comentarios
Etiquetas:
ARTÍCULO ANTERIOR
Remover el import de react from react al usar los snippets de ES7 React, Redux, React-Native Snippets
15 49.0138 8.38624 1 0 4000 1 https://www.cesarguerra.mx 300 0