高さをなるべく揃えてくれる React Masonry コンポーネントライブラリを作った

1 min read読了の目安(約1300字

https://github.com/mtsmfm/react-masonry

https://codesandbox.io/s/mtsmfmreact-masonry-example-jeh6k

Masonry とは

サイズがまちまちなコンテンツをタイル状に並べるときに、縦を詰めるようにするレイアウト。

react-masonry-css では駄目なのか

react-masonry-css では要素の高さを計算しているわけではなく、単純に左上から順に並べている。この手法だと、ある程度の数が並ぶと各列の最後の要素の位置がずれてしまう。

つまり、

1 2 3
1 2
1

なら次の要素は 3 の下に来ないといけないが単純に並べているだけなので

1 2 3
1 2 6
1 5
4

となってしまう。これが積み重なるとスクロールし終えたときに高さがかなり違う。

拙作のライブラリでは

1 2 3
1 2 4
1 5 6

となる。

ただし、高さを計算する分重い。

実現方法

  1. 各要素の高さを ResizeObserver + getBoundingClientRect で取得する
  2. 取得した高さを元に、各列がなるべく同じ高さになるように並べる
  3. 並べ終わって再 render する際にも ResizeObserver に引っかかってしまってまた再計算となるととても重くなるので、再計算は throttle する

検討した CSS たち

display: grid

grid を細かく (1px とか) しまくって CSS だけでなんとかするアプローチ。
grid には行数制限があるため駄目だった。

flexbox (row)

各行の高さが一律になってしまう。

flexbox (column)

高さの計算が少しでも狂うと崩壊する。

flexbox (row + コンテナ複数)

DOM ツリーが変わってしまうため css transition が使えない。

position: absolute

高さの計算をミスると悲惨なことになるが column ほどではない。
最終的にはこれを使った。

番外

grid-template-rows: masonry; さえくれば CSS で全部済むのでこんなライブラリは不要になる。

https://github.com/w3c/csswg-drafts/blob/0c1a35dd5d938c9d6e5d11cd144f5f45c4460d4a/css-grid-2/MASONRY-EXPLAINER.md