hand Inicio
hand JSBloqs
hand GutenBloqs
Qué?
nochedía

DESARROLLO WEB con
REACT y WORDPRESS

Apúntate a la newsletter (escribo algo de tanto en cuanto)
Wallpaper)

Implementar un grid masonry con React

500 palabras
2 minutos
July 8, 2020
curso, cursosgatsbycssmasonry

Una distribución masonry de momento no la tenemos de serie con css, con lo que tenemos que o bien utilizar alguna librería, o bien hacer una solución manual

Aquí vamos a explorar la opciones manual con grid

La Técnica Masonry

Distribuir elementos de tipo masonry quiere decir sin ninguna cuadrícula concreta sino ir añadiendo elementos a las columnas más vacías e ir rellenándolas a medida

El efecto es el que consiguen sitios como pinterest

Y por qué no utilizar lo tradicional? Lo máximo que podemos hacer con css-grid es esto

Y un masonry sería esto

Cómo lo hacemos? Con JavaScript en formato React y utilizando css-grid

Vamos allá

  1. La Técnica Masonry
  2. JavaScript con css-grid
  3. Otras opciones en formato librería?

JavaScript con css-grid

Para implementar el masonry con el css-grid se trata de tejer una malla con filas relativamente estrechas, y luego definir cada imagen para que ocupe un número determinado de imágenes

(inspirado en esta implementación)

De momento nos centramos en leer la altura de cada imagen

jsx
// importamos las librerías
import React, { useState, useLayoutEffect, useCallback } from 'react'
import styled from '@emotion/styled'
// construimos el array de imágenes y lo asignamos al estado
const useImages = num => {
const [items, setItems] = useState([]) // guardaremos las imágenes aquí
// con useLayoutEffect nos aseguramos que este código sólo lo ejecutamos una vez
// .. y justo antes de que el DOM esté listo
useLayoutEffect(() => {
// almacenamos en el estado una lista de 20 imágenes random
setItems(
Array.from({ length: num - 1 }, () => {
const rand = Math.random()
const r = rand > 0.1 ? parseInt(500 * rand) : parseInt(500 * 0.1)
return `https://picsum.photos/200/${r}`
})
)
}, [])
return [items]
}
// utilizamos este hook para calcular las dimensiones de cada imagen
const useClientRect = load => {
const [rect, setRect] = useState() // asignamos un estado para las dimensiones
// useCallback nos memoriza la función (no su valor)
const ref = useCallback(
el => {
if (el !== null) {
console.log(el.getBoundingClientRect())
setRect(el.getBoundingClientRect()) // nos devuelve las dimensiones del elemento
}
},
[load]
) // las dependencias incluyen la variable load
return [rect, ref]
}
export default () => {
const [images] = useImages(20) // crearemos 20 imágenes
return (
<Grid>
{images.map((el, i) => (
<Card key={i} num={i} src={el} />
))}
</Grid>
)
}
// Genera una imagen y un texto
const Card = ({ num, src }) => {
const [load, loaded] = useState({}) // un estado que cambiará cuando la imagen se cargue
const [rect, ref] = useClientRect(load)
return (
<CardDiv ref={ref}>
<Label>{num}</Label>
<Img src={src} alt="" onLoad={() => loaded({})} />
</CardDiv>
)
}
const Img = styled.img`
border: 1px solid #000;
min-height: 50px;
margin: 0;
padding: 0;
`
const Label = styled.div`
position: absolute;
color: #fff;
font-weight: 700;
font-size: 3em;
`
const CardDiv = styled.div`
box-sizing: content-box;
background: #eaeaea;
border-radius: 8px;
`
const Grid = styled.div`
display: grid;
grid-gap: 10px;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
`
Ver en CodeSandBox

Con éste código

  • Hemos generado una lista de 20 imágenes con la función (un custom hook) useImages

Este hook simplemente almacena en un useState una lista que genera sólo una vez con useLayoutEffect y que se trata de direcciones distintas de picsum.photos

  • Con useClientRect recogemos las dimensiones de las imágenes que se actualizan una vez se cargan

Utilizamos una función como referencia, esa función la memoizamos con useCallback, y recalculamos la función cuando la imagen se carga

Esto último lo hacemos con un estado load loaded al cual le asignamos un nuevo objeto {} simplemente para cambiarle el estado y forzar la actualización

De momento no hacemos nada excepto imprimir el valor en la consola (si lo miras en el codesandbox recuerda que debes añadir las dependencias @emotion/styled y @emotion/code)

Ahora se trata de utilizar esas dimensiones (sólo nos interesa el height) para asignar un número de filas a esa imagen

Esto lo haremos simplemente dividiendo el height de la imagen por el valor que le damos a cada fila

Esa fila, cuanto más estrecha más precisión pero también más necesidad de cálculo, con lo que es un compromiso

Aquí la definimos de 20px

jsx
// importamos las librerías
import React, { useState, useLayoutEffect, useEffect, useCallback } from 'react'
import styled from '@emotion/styled'
// construimos el array de imágenes y lo asignamos al estado
const useImages = num => {
const [items, setItems] = useState([]) // guardaremos las imágenes aquí
// con useLayoutEffect nos aseguramos que este código sólo lo ejecutamos una vez
// .. y justo antes de que el DOM esté listo
useLayoutEffect(() => {
// almacenamos en el estado una lista de 20 imágenes random
setItems(
Array.from({ length: num - 1 }, () => {
const rand = Math.random()
const r = rand > 0.1 ? parseInt(500 * rand) : parseInt(500 * 0.1)
return `https://picsum.photos/200/${r}`
})
)
}, [])
return [items]
}
// utilizamos este hook para calcular las dimensiones de cada imagen
const useClientRect = load => {
const [rect, setRect] = useState() // asignamos un estado para las dimensiones
// useCallback nos memoriza la función (no su valor)
const ref = useCallback(
el => {
if (el !== null) setRect(el.getBoundingClientRect()) // nos devuelve las dimensiones del elemento
},
[load]
) // las dependencias incluyen la variable load
return [rect, ref]
}
export default () => {
const [images] = useImages(20) // crearemos 20 imágenes
const defaultGridRow = 20 // cada fila será de 20 píxeles
const columnWidth = 250 // cada fila la defino de 250 px
const imagesGap = 5 // Hay un espacio mínimo entre imágenes (vertical) de 5px
return (
<Grid row={defaultGridRow} col={columnWidth}>
{images.map((el, i) => (
<Card key={i} num={i} src={el} row={defaultGridRow} gap={imagesGap} />
))}
</Grid>
)
}
// Genera una imagen y un texto
const Card = ({ num, src, row, gap }) => {
const [load, loaded] = useState({}) // un estado que cambiará cuando la imagen se cargue
const [rect, ref] = useClientRect(load)
const [gridRows, setGridRows] = useState(1)
useEffect(() => {
rect && setGridRows(Math.ceil(rect.height / parseInt(row)))
}, [rect])
return (
<CardDiv span={gridRows}>
<div ref={ref}>
<Label>{num}</Label>
<Img src={src} alt="" onLoad={() => loaded({})} gap={gap}/>
</div>
</CardDiv>
)
}
const Img = styled.img`
border: 1px solid #000;
min-height: 50px;
margin: 0;
margin-bottom: ${props => props.gap}px;
padding: 0;
`
const Label = styled.div`
position: absolute;
color: #fff;
font-weight: 700;
font-size: 3em;
`
const CardDiv = styled.div`
box-sizing: content-box;
background: #eaeaea;
border-radius: 8px;
grid-row-end: span ${props => props.span};
`
const Grid = styled.div`
display: grid;
grid-gap: 0px; /* no puede haber gap entre filas */
grid-template-columns: ${props => `repeat(auto-fill, minmax(${props.col}px, 1fr))`};
grid-auto-rows: ${props => props.row}px;
`
Ver en CodeSandBox

Y ya lo tenemos

Con este código

  • Una vez tenemos el height de las imágenes, calculamos cuántas filas ocuparía en el grid

Y este valor lo calculamos cada vez que se actualizan las dimensiones mediante un useEffect, y almacenamos el valor en un useState

  • Y pasamos ese valor al componente css para que lo incluya en un grid-row-end: span X

El resto de cambios son para poder definir las variables de forma más cómoda en el código y que después estos se vayan al css, y listos

El único detalle interesante es que he tenido que añadir un <div> extra que antes no estaba entre la imagen y el <CardDiv>

Esto lo he tenido que hacer porque sino no podía medir su height ya que este venía dado por el grid

Con un div intermedio, este div es el que quedaba bloqueado, y la imagen bajo este podía expresar su height con libertad

Por lo demás, sólo faltaría pulir la estética final para centrar los elementos

Por ejemplo, cambiando ligeramente los estilos

js
const Img = styled.img`
box-shadow: 1px 1px 1px #d2d2d2;
border-radius: 8px;
min-height: 50px;
margin: 0;
margin-bottom: ${props => props.gap}px;
`
const Label = styled.div`
position: absolute;
color: #fff;
font-weight: 700;
font-size: 3em;
margin: 0px 0px 0px 15px;
`
const CardDiv = styled.div`
box-sizing: content-box;
border-radius: 8px;
grid-row-end: span ${props => props.span};
justify-self: center;
`
const Grid = styled.div`
display: grid;
grid-gap: 0px; /* no puede haber gap entre filas */
grid-template-columns: ${props => `repeat(auto-fill, minmax(${props.col}px, 1fr))`};
grid-auto-rows: ${props => props.row}px;
`

Para terminar de esta manera

Otras opciones en formato librería?

Las tienes en esta entrada

🙋‍♂️

Qué tal el curso?

👌 Bien 🙌🙌
👍 Bien, pero algunas cosas podrían explicarse mejor 😬
🤷‍♂️ Da por sentadas demasiadas cosas 😒
🤷‍♂️ A ver, hay poca chicha 😬
🤷‍♂️ Los ejemplos no son muy claros 🙇‍♂️
🤷‍♂️ No se entiende, está mal escrito 👎
✍️ Hay errores, revísalo en cuanto puedas 🙏
Enviar Feedback ✍️
El texto está en blanco!
Gracias por enviarme tu opinión
👍

Si quieres explorar más cursos y más entradas en el blog, los tienes todos en la página principal, y si el contenido te ha ayudado dame las gracias por ejemplo por twitter con este enlace 👍

Privacidad
by kuworking.com
[ 2020 >> kuworking ]