react-responsive-container

232 阅读2分钟

前言

最近接到一个需求大概内容是这样的:页面布局中各个模块需要是可拖拽的,可响应式的,即可拖动改变每个容器大小来实现各自模块的UI响应式。即各自模块有自己的 breakpoints 断点,例如:sm, md, lg, xl

Demo

responsive.gif

分析

可拖拽和可响应式的这个用业界开源的库结合使用就可以:
react-draggable: 元素可拖拽
re-resizable: 元素可 resize

我们需要自己实现一个根据容器宽度进行断点的容器组件
假设我们设计的断点如下:

const breakpoints = [375, 800, 1200];
const breakpointNames = ['sm', 'md', 'lg', 'xl'];

  1. 当容器宽度小于 375 时, bp="sm",显示小屏幕内容。
  2. 当宽度在 375 到 800 之间时, bp="md",显示中屏幕内容。
  3. 当宽度在 800 到 1200 之间时, bp="lg",显示大屏幕内容。
  4. 当宽度大于 1200 时, bp="xl",显示超大屏幕内容。

这里计算容器的宽度需要用到 ResizeObserver

实现

最终代码如下:

import React, { useState, useEffect, useRef, Children, cloneElement, isValidElement } from 'react';
import PropTypes from 'prop-types';

// 定义断点名称
const breakpointNames = ['sm', 'md', 'lg', 'xl'];

// 根据宽度计算断点
const calculateBreakpoint = (width, breakpoints, breakpointNames) => {
  for (let i = 0; i < breakpoints.length; i++) {
    if (width < breakpoints[i]) {
      return breakpointNames[i] || '';
    }
  }
  return breakpointNames[breakpoints.length] || ''; // 超过最大断点
};

const ResponsiveContainer = ({ children, breakpoints, nested = false }) => {
  const containerRef = useRef(null);
  const [currentBreakpoint, setCurrentBreakpoint] = useState('');

  useEffect(() => {
    const container = containerRef.current;

    if (!container) return;

    const resizeObserver = new ResizeObserver((entries) => {
      for (const entry of entries) {
        const { width } = entry.contentRect;
        const newBreakpoint = calculateBreakpoint(width, breakpoints, breakpointNames);
        if (newBreakpoint !== currentBreakpoint) {
          setCurrentBreakpoint(newBreakpoint);
        }
      }
    });

    // 监听容器尺寸变化
    resizeObserver.observe(container);

    return () => {
      // 清除监听
      resizeObserver.disconnect();
    };
  }, [breakpoints, currentBreakpoint]);

  // 递归为所有子组件注入 bp 属性
  const injectBreakpoint = (children) => {
    return Children.map(children, (child) => {
      if (isValidElement(child)) {
        // 如果子组件有嵌套子组件,递归处理
        const childProps = {
          bp: currentBreakpoint,
          // 是否需要给 所有嵌套的子组件也传入 bp,否则只给第一级子组件传入 bp 属性
          children: nested ? injectBreakpoint(child.props.children) : child.props.children,
        };
        return cloneElement(child, childProps);
      }
      return child; // 如果不是有效的 React 元素,直接返回
    });
  };

  return (
    <div ref={containerRef} className={`responsive-container ${currentBreakpoint}`}>
      {injectBreakpoint(children)}
    </div>
  );
};

ResponsiveContainer.propTypes = {
  breakpoints: PropTypes.arrayOf(PropTypes.number).isRequired,
  children: PropTypes.node.isRequired,
};

export default ResponsiveContainer;

使用demo

import React from 'react';
import ResponsiveContainer from './ResponsiveContainer';

const NestedChild = ({ bp }) => {
  return <p>嵌套子组件,当前断点: {bp}</p>;
};

const ChildComponent = ({ bp, children }) => {
  return (
    <div>
      <h1>子组件,当前断点: {bp}</h1>
    
      {bp === 'sm' && <p>小屏幕内容</p>}

      {bp === 'md' && <p>中屏幕内容</p>}

      {bp === 'lg' && <p>大屏幕内容</p>}

      {bp === 'xl' && <p>超大屏幕内容</p>}

      {children}
    </div>
  );
};

const App = () => {
  return (
    <ResponsiveContainer breakpoints={[375, 800, 1200]} nested>
      <ChildComponent>
        <NestedChild />
        <div><NestedChild /></div>
      </ChildComponent>
    </ResponsiveContainer>
  );
};

export default App;

总结

一个简单的响应式容器组件就实现了,子组件可以根据 bp 属性去渲染对应的响应式内容了