react的响应式容器组件-基于容器的宽度来决定显示的子元素数量

背景

最近接到了一个需求,基于容器的宽度来决定显示的子元素数量。如果屏幕或容器足够宽,它将展示更多的子元素;如果屏幕较窄,它将隐藏一些子元素,并提供一个下拉菜单来查看这些被隐藏的内容

本来想偷懒,用gpt来做嘛,但是gpt做出来的组件库对于功能的满足不能说十全十美吧,只能一无是处,试了两个小时,放弃了,还不如自己写呢

说干就干!!!

实现思路

  1. 组件属性定义&初始化状态
  2. 动态计算和更新可见子元素数量
  3. 窗口尺寸变化的处理
  4. 渲染可见和隐藏的子元素
  5. 组件的最终渲染

既然有了实现思路我们就开始实现吧

实现

组件属性定义&初始化状态

  • 组件属性定义ResponsiveContainer 组件接收两个属性:childrenlimitchildren 是任意的 React 节点,而 limit 是一个可选属性,用于限制显示子元素的数量。如果没有给定 limit,组件将基于容器的宽度动态确定可见的子元素数量。
  • 初始化状态 : 组件内部使用 useRef 创建两个引用:containerRefshadowRefcontainerRef 用于引用实际显示内容的容器,而 shadowRef 用于临时存放所有子元素以计算它们的总宽度。同时,使用 useState 创建一个 visibleCount 状态,用来存储应该显示的子元素数量。

组件属性定义

kotlin 复制代码
interface ResponsiveContainerProps {
  children: ReactNode; // 期望传入任意的React节点作为children
  limit?: number; // 可选属性,限制显示的子元素数量;如果未设置,将基于容器宽度动态决定
}

初始化状态

ini 复制代码
  const containerRef = useRef<HTMLDivElement>(null);
  const [visibleCount, setVisibleCount] = useState(0);
  const shadowRef = useRef<HTMLDivElement>(null);

动态计算和更新可见子元素数量

updateChildrenVisibility 函数负责计算在当前容器宽度下,可以显示多少子元素。如果提供了 limit 属性,则直接使用 limit 设定的值更新 visibleCount 状态。如果没有提供 limit,函数将遍历 shadowRef 中的子元素,累加它们的宽度,直到达到 containerRef 容器的宽度限制。这个过程中还考虑了额外预留的空间(如12像素),确保内容不会溢出容器。

ini 复制代码
  // 更新子元素的可见性
  const updateChildrenVisibility = useCallback(() => {
    // 如果limit有值,将可见子元素数量设置为limit,否则,根据容器的宽度和子元素的宽度计算可见子元素数量
    if (limit !== undefined && limit !== null) {
      setVisibleCount(limit);
    } else {
      if (shadowRef.current && containerRef.current) {
        // 累加宽度初始化为0
        let currentWidth = 0;
        // 获取容器的最大宽度
        const { width: maxWidth } =
          containerRef.current.getBoundingClientRect();
        const count = shadowRef.current.childElementCount;
        const breadcrumbChildren = shadowRef.current.children;

        let localCount = 0;
        for (let i = 0; i < count; i++) {
          const child = breadcrumbChildren[i];
          const { width } = child.getBoundingClientRect();
          // 如果当前宽度加上当前子元素的宽度超过了容器的最大宽度就结束循环
          if (maxWidth - 12 <= currentWidth + width) {
            break;
          } else {
            //否则,根据容器的宽度和子元素的宽度计算可见子元素数量
            currentWidth += width;
            localCount += 1;
          }
        }
        setVisibleCount(localCount);
      }
    }
  }, [limit]);

窗口尺寸变化的处理

使用 useEffect 钩子监听窗口大小的变化。当窗口大小改变时,调用 updateChildrenVisibility 函数来重新计算可见的子元素数量。这样确保了响应式行为,能适应窗口或容器宽度的变更。

scss 复制代码
  // 使用useEffect来处理窗口尺寸变化事件
  useEffect(() => {
    updateChildrenVisibility();
    const handleResize = () => updateChildrenVisibility();
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, [children, limit]);

渲染可见和隐藏的子元素

组件使用 React.Children.toArraychildren 转换为数组,并根据 visibleCount 状态来拆分为两个数组:visibleChildrenhiddenChildrenvisibleChildren 包含那些将会被直接渲染在容器中的子元素,对于 hiddenChildren,组件使用 Dropdown 菜单来提供访问隐藏子元素的方式。当用户与 Dropdown 交互时(例如鼠标悬停),隐藏的子元素将作为下拉菜单项显示出来。

ini 复制代码
  const visibleChildren = React.Children.toArray(children).slice(
    0,
    visibleCount,
  );
  const hiddenChildren = React.Children.toArray(children).slice(visibleCount);
  // 准备Dropdown菜单内容
  const menu = (
    <Menu>
      {hiddenChildren.map((child, index) => (
        <Menu.Item key={index}>{child}</Menu.Item>
      ))}
    </Menu>
  );

最终渲染

组件返回了一个包裹 wrappershadow 两部分的 JSX 结构。wrapper 用于显示可见的子元素,而 shadow(虽然被设置为不可见)用于帮助计算子元素的宽度。Dropdown 菜单被放置在 wrapper 中,以便用户可以交互。

必须将visibility: hidden,而不是使用display: none。当我们给一个元素设置display: none时,这个元素会从文档流中完全消失。这意味着它不占据任何空间,也不会渲染任何内容,因此无法进行宽度测量

less 复制代码
 return (
    <div ref={containerRef} style={{ position: 'relative', width: '100%' }}>
      <div
        className="wrapper"
        style={{
          display: 'flex',
          overflow: 'hidden',
          flexWrap: 'nowrap',
          width: '100%',
        }}
      >
        {visibleChildren.map((child, index) => (
          <div key={index} style={{ display: 'inline-block' }}>
            {child}
          </div>
        ))}
        {hiddenChildren.length > 0 && (
          <Dropdown overlay={menu} trigger={['hover']} arrow>
            <EllipsisOutlined style={{ fontSize: 16 }} />
          </Dropdown>
        )}
      </div>
      <div
        className="shadow"
        ref={shadowRef}
        style={{
          position: 'absolute',
          zIndex: -1,
          visibility: 'hidden',
          flexWrap: 'nowrap',
          display: 'flex',
        }}
      >
        {children}
      </div>
    </div>
  );

所有代码

ini 复制代码
import { Dropdown, Menu } from 'antd';
import React, {
  ReactNode,
  useCallback,
  useEffect,
  useRef,
  useState,
} from 'react';
import { EllipsisOutlined } from '@ant-design/icons';

// 定义组件的Props类型
interface ResponsiveContainerProps {
  children: ReactNode; // 期望传入任意的React节点作为children
  limit?: number; // 可选属性,限制显示的子元素数量;如果未设置,将基于容器宽度动态决定
}

const ResponsiveContainer: React.FC<ResponsiveContainerProps> = ({
  children,
  limit,
}) => {
  const containerRef = useRef<HTMLDivElement>(null);
  const [visibleCount, setVisibleCount] = useState(0);
  const shadowRef = useRef<HTMLDivElement>(null);

  // 更新子元素的可见性
  const updateChildrenVisibility = useCallback(() => {
    if (limit !== undefined && limit !== null) {
      setVisibleCount(limit);
    } else {
      if (shadowRef.current && containerRef.current) {
        let currentWidth = 0;
        const { width: maxWidth } =
          containerRef.current.getBoundingClientRect();
        const count = shadowRef.current.childElementCount;
        const breadcrumbChildren = shadowRef.current.children;

        let localCount = 0;
        for (let i = 0; i < count; i++) {
          const child = breadcrumbChildren[i];
          const { width } = child.getBoundingClientRect();
          if (maxWidth - 12 <= currentWidth + width) {
            break;
          } else {
            currentWidth += width;
            localCount += 1;
          }
        }
        setVisibleCount(localCount);
      }
    }
  }, [limit]);


  // 使用useEffect来处理窗口尺寸变化事件
  useEffect(() => {
    updateChildrenVisibility();
    const handleResize = () => updateChildrenVisibility();
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, [children, limit]);

  const visibleChildren = React.Children.toArray(children).slice(
    0,
    visibleCount,
  );
  const hiddenChildren = React.Children.toArray(children).slice(visibleCount);
  // 准备Dropdown菜单内容
  const menu = (
    <Menu>
      {hiddenChildren.map((child, index) => (
        <Menu.Item key={index}>{child}</Menu.Item>
      ))}
    </Menu>
  );

  return (
    <div ref={containerRef} style={{ position: 'relative', width: '100%' }}>
      <div
        className="wrapper"
        style={{
          display: 'flex',
          overflow: 'hidden',
          flexWrap: 'nowrap',
          width: '100%',
        }}
      >
        {visibleChildren.map((child, index) => (
          <div key={index} style={{ display: 'inline-block' }}>
            {child}
          </div>
        ))}
        {hiddenChildren.length > 0 && (
          <Dropdown overlay={menu} trigger={['hover']} arrow>
            <EllipsisOutlined style={{ fontSize: 16 }} />
          </Dropdown>
        )}
      </div>
      <div
        className="shadow"
        ref={shadowRef}
        style={{
          position: 'absolute',
          zIndex: -1,
          visibility: 'hidden',
          flexWrap: 'nowrap',
          display: 'flex',
        }}
      >
        {children}
      </div>
    </div>
  );
};

参考

相关推荐
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端
爱敲代码的小鱼9 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax