JR

Pages

Blog Posts

Powered by Algolia
nature-photos-masonry

React Hooks Masonry

Mar 14, 20191 min readDesign, Web Dev, Tutorial, JS

Now that we have React Hooks, so many components can (and should) be rewritten in a more succinct, readable and maintainable manner (despite what Dan said at React Conf). A perfect candidate for this in my own code base was a rather brittle Masonry component that was centered around CSS grid. With hooks, it was easy to significantly improve on this approach.

For those unfamiliar with “masonry” on the web, the goal is to create a layout like this.

1
4
7
10
13
16
2
5
8
11
14
17
3
6
9
12
15
18

Excluding the useEventListener hook and styles it uses, the new implementation uses ony 29 lines of codes and is about as plug-and-play as components get.

src/components/masonry/index.js
import React, { useEffect, useRef, useState } from 'react'
import { useEventListener } from 'hooks'
import { Col, MasonryDiv } from './styles'

const fillCols = (children, cols) => {
  children.forEach((child, i) => cols[i % cols.length].push(child))
}

export default function Masonry({ children, gap, minWidth = 500, ...rest }) {
  const ref = useRef()
  const [numCols, setNumCols] = useState(3)
  const cols = [...Array(numCols)].map(() => [])
  fillCols(children, cols)

  const resizeHandler = () =>
    setNumCols(Math.ceil(ref.current.offsetWidth / minWidth))
  useEffect(resizeHandler, [])
  useEventListener(`resize`, resizeHandler)

  return (
    <MasonryDiv ref={ref} gap={gap} {...rest}>
      {[...Array(numCols)].map((_, index) => (
        <Col key={index} gap={gap}>
          {cols[index]}
        </Col>
      ))}
    </MasonryDiv>
  )
}

The styled components MasonryDiv and Col each create a CSS grid to layout children with a default gap of 1em. You can pass any valid CSS units as a string as well as different values for vertical and horizontal gap, e.g. <Masonry gap="calc(2vh + 20px) calc(1vw + 20px)" />.

src/components/masonry/styled.js
import styled from 'styled-components'

export const MasonryDiv = styled.div`
  display: grid;
  grid-auto-flow: column;
  grid-gap: ${props => props.gap || `1em`};
`

export const Col = styled.div`
  display: grid;
  grid-gap: ${props => props.gap || `1em`};
`

Examples

Using Masonry is as simple as wrapping it around an array of child elements. Here’s how you’d use it to display a list of image thumbnails.

import React from 'react'
import Masonry from 'components/Masonry' 
import { Thumbnail } from './styles'

export const Photos = ({ photos }) => (
  <Masonry>
    {photos.map(img => (
      <Thumbnail key={img.title} alt={img.title} src={img.src} />
    ))}
  </Masonry> 
)

More concretely, the above colored tiles are rendered by the this component.

ColorMasonry.js
import React, { useState } from 'react'
import Masonry from 'components/Masonry' 
import shuffle from 'lodash/shuffle'
import styled from 'styled-components'

const ColorBox = styled.div`
  border-radius: 1em;
  transition: 0.2s;
  justify-content: center;
  align-content: center;
  display: grid;
  color: white;
  cursor: pointer;
  :hover {
    transform: scale(1.1);
    box-shadow: 0 0 12px 0 var(--color-shadow);
  }
`

const data = [
  [`5em`, `linear-gradient(45deg, #f05f70, #164b78)`],
  [`2em`, `linear-gradient(45deg, #5cb767, #2e9fff)`],
  [`4em`, `linear-gradient(45deg, #e0c3fc, #8ec5fc)`],
  [`7em`, `linear-gradient(45deg, #f093fb, #f5576c)`],
  [`1em`, `linear-gradient(45deg, #ffd34f, #2e9fff)`],
  [`3em`, `linear-gradient(45deg, #d299c2, #fef9d7)`],
  [`2em`, `linear-gradient(45deg, #f6d365, #fda085)`],
  [`5em`, `linear-gradient(45deg, #164b78, #ffd34f)`],
  [`5em`, `linear-gradient(45deg, #96fbc4, #f9f586)`],
]

export default function MasonryExample() {
  const [divs, setDivs] = useState(data.concat(data))
  return (
    <Masonry minWidth={300} css="margin: 2em 0;">
      {divs.map(([minHeight, background], index) => (
        <ColorBox
          key={index}
          style={{ background, minHeight }}
          onClick={() => setDivs(shuffle)}
        >
          {index + 1}
        </ColorBox>
      ))}
    </Masonry> 
  )
}
© 2020 - Janosh RiebesellRSSThis site is open source
Powered by