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>
  );
};

参考

相关推荐
阿伟来咯~22 分钟前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端27 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱30 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai39 分钟前
uniapp
前端·javascript·vue.js·uni-app
也无晴也无风雨40 分钟前
在JS中, 0 == [0] 吗
开发语言·javascript
bysking2 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4112 小时前
无网络安装ionic和运行
前端·npm
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js