使用 React 实现无限滚动加载的瀑布流布局

240 阅读3分钟

瀑布流布局是一种流行的布局方式,用于显示内容。它们通常用于 Web 应用程序中,以有序且视觉上吸引人的方式展示图像、视频和其他类型的内容。本文将使用 React 框架实现瀑布流布局。

什么是瀑布流布局?

瀑布流布局(Waterfall Layout),又称瀑布流式布局或砌体布局(Masonry Layouts),是一种流行的网页或移动端界面设计形式。其核心特点是多列等宽不等高的元素参差排列,视觉上像瀑布一样错落有致,随着页面滚动,新内容不断加载并附加到尾部。

瀑布流布局的优势主要体现在以下几个方面:空间利用率方面,它能紧凑排列高度不一的元素,减少空白区域,提高信息密度。用户体验方面,它能提供沉浸式浏览体验,通过无限滚动减少用户操作成本。视觉吸引力方面,错落有致的布局缓解视觉疲劳,增强探索趣味性。

实现效果

2025-08-24182628-ezgif.com-video-to-webp-converter.webp

前置准备

在开始构建瀑布流布局之前,我们先使用 vite 创建一个 React 项目。如果你有现成的 React 项目,可以跳过此步骤。

npm create vite@latest

按照提示创建一个新的 React 项目。项目创建完成后,在项目的根目录安装 Magic-Grid 库。

npm install magic-grid

安装 react-infinite-scroll-hook 库来实现无限滚动。

npm install react-infinite-scroll-hook

实现瀑布流布局

创建自定义 useMagicGrid hook

为了在 React 中使用 Magic-Grid 库,我们需要创建一个 useMagicGrid hook,这里不用官方版本的 use-magic-grid 是因为官方版本的库不兼容 React 19。

// use-magic-grid.ts
import { useRef, useEffect } from "react"
import MagicGrid, { type MagicGridProps } from "magic-grid"

type useMagicGridProps = Omit<MagicGridProps, "static">

const useMagicGrid = (props: useMagicGridProps): MagicGrid => {
  const gridRef = useRef<MagicGrid>(null)
  const { container } = props

  useEffect(() => {
    if (!container) {
      throw new Error("Container name or element is required")
    }

    if (!gridRef.current) {
      gridRef.current = new MagicGrid({
        ...props,
        items: props.items || 1,
        static: false,
      })
      gridRef.current.listen()
      return
    }

    const grid = gridRef.current
    const currentContainer = document.querySelector(grid.containerClass)
    const containerChanged = grid.container !== currentContainer

    if (currentContainer && containerChanged) {
      grid.setContainer(currentContainer as HTMLElement)
    }
  })

  return gridRef.current as MagicGrid
}

export { useMagicGrid }

代码实现

在 App.tsx 文件中,添加以下代码:

// App.tsx
import { useState, useLayoutEffect } from "react"
import useInfiniteScroll from "react-infinite-scroll-hook"
import { useMagicGrid } from "./use-magic-grid"
import "./app.css"

const ARRAY_SIZE = 20
const RESPONSE_TIME_IN_MS = 1000

export interface Item {
  key: number
  content: string
  height: number
}

interface Response {
  hasNextPage: boolean
  data: Item[]
}

// 获取随机数
function getRandomNumber(min: number, max: number) {
  const randomIntBetween = Math.floor(Math.random() * (max - min + 1)) + min
  return randomIntBetween
}

function loadItems(startCursor = 0): Promise<Response> {
  return new Promise((resolve) => {
    let newArray: Item[] = []

    setTimeout(() => {
      for (let i = startCursor; i < startCursor + ARRAY_SIZE; i++) {
        // 随机生成卡片的高度
        const randomIntBetween = getRandomNumber(100, 500)
        const newItem = {
          key: i + 1,
          content: `item ${(i + 1).toString()}`,
          height: randomIntBetween,
        }
        newArray = [...newArray, newItem]
      }

      resolve({ hasNextPage: true, data: newArray })
    }, RESPONSE_TIME_IN_MS)
  })
}

function App() {
  const [loading, setLoading] = useState(false)
  const [items, setItems] = useState<Item[]>([])
  const [hasNextPage, setHasNextPage] = useState<boolean>(true)
  const [error, setError] = useState<Error>()

  const magicGrid = useMagicGrid({
    container: ".items",
    items: items.length,
    animate: true,
    useMin: true,
    useTransform: false,
  })

  async function loadMore() {
    setLoading(true)
    try {
      const { data, hasNextPage: newHasNextPage } = await loadItems(
        items.length
      )
      setItems((current) => [...current, ...data])
      setHasNextPage(newHasNextPage)
    } catch (error_) {
      setError(error_ instanceof Error ? error_ : new Error("加载错误!"))
    } finally {
      setLoading(false)
    }
  }

  const [infiniteRef] = useInfiniteScroll({
    loading,
    hasNextPage,
    onLoadMore: loadMore,
    disabled: Boolean(error),
    rootMargin: "0px 0px 400px 0px",
  })

  // 使用 useLayoutEffect 避免向下滚动时,可能出现的闪烁问题
  useLayoutEffect(() => {
    if (magicGrid) {
      // 在容器中新增列表项时调用,重新计算布局
      magicGrid.positionItems()
    }
  }, [items])

  return (
    <div className="app-container">
      <div className="items">
        {items.map((item) => (
          <div key={item.key} className="item" style={{ height: item.height }}>
            {item.content}
          </div>
        ))}
      </div>
      {hasNextPage && (
        <div ref={infiniteRef} className="loading">
          加载中...
        </div>
      )}
    </div>
  )
}

export default App

我们还需要添加对应的css代码:

// app.css
.app-container {
  padding: 20px;
}

.item {
  width: 280px;
  height: 500px;
  background-color: antiquewhite;
  display: flex;
  justify-content: center;
  align-items: center;
  border-radius: 8px;
}

.loading {
  display: flex;
  justify-content: center;
}

总结

本文使用了 magic-grid 库在 React 中构建瀑布流布局。瀑布流布局是一种以网格状结构展示内容的绝佳方式,能够适应不同的屏幕尺寸和内容类型。通过在网页应用中实现瀑布流布局,你可以创建出具有视觉吸引力的设计来吸引用户,提升用户的使用体验。