You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

宽度感知React组件侧边栏切换时出现渲染跳动问题求助

解决侧边栏切换时React组件布局跳动问题

问题场景

我通过ResizeObserver实现了一个可感知自身宽度的React组件,内部两个子div均设置flex: 1以平分容器宽度。当组件宽度小于700px且处于编辑状态(isEditing为true)时,会隐藏dashboard_monitorsdiv,让dashboard_activitydiv占据全部宽度。

但应用包含可开合侧边栏,关闭侧边栏后组件宽度恢复,本应同步显示dashboard_monitors,实际却会先绘制一次(此时dashboard_activity仍占满宽度),之后组件才更新状态显示dashboard_monitors,导致页面出现明显跳动。

核心原因

ResizeObserver的回调是异步触发的,侧边栏切换导致组件宽度变化后,ResizeObserver不会立即更新dashboardWidth状态,React的渲染周期滞后于DOM宽度变化,从而出现两次绘制的跳动现象。

解决方案

1. 用CSS过渡优化布局变化

将display: none替换为visibility: hidden配合flex属性控制,让元素在隐藏时仍保留布局空间,同时添加过渡动画让变化平滑:

.dashboard {
    display: flex;
    height: 100%;
    transition: all 0.2s ease;
}

.dashboard.single .dashboard_monitors {
  visibility: hidden;
  flex: 0 0 0;
  opacity: 0;
  pointer-events: none;
}

.dashboard.single .dashboard_activity {
  margin-left: 0;
  flex: 1 1 100%;
}

.dashboard > * {
  flex: 1;
  overflow-x: hidden;
  overflow-y: auto;
  transition: flex 0.2s ease, opacity 0.2s ease, visibility 0.2s ease;
}

.dashboard_activity {
  margin-left: var(--padding-2);
}

.dashboard_monitors {
    display: grid;
    gap: var(--padding-2);
    grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
    grid-auto-rows: 200px;
    padding-right: var(--padding-0);
    padding-bottom: var(--padding-0);
}

2. 优化useWidth Hook的更新时机

让Hook初始化时主动获取宽度,并用requestAnimationFrame确保状态更新与浏览器重绘同步,同时添加主动刷新方法:

import { useState, useRef, useEffect } from "react";

export const useWidth = <T extends HTMLElement>() => {
    const [width, setWidth] = useState(0);
    const ref = useRef<T>(null);
    const observerRef = useRef<ResizeObserver | null>(null);

    useEffect(() => {
        const updateWidth = (entries: ResizeObserverEntry[]) => {
            requestAnimationFrame(() => {
                setWidth(entries[0].contentRect.width);
            });
        };

        observerRef.current = new ResizeObserver(updateWidth);
        const refElement = ref.current;

        if (refElement) {
            observerRef.current.observe(refElement);
            setWidth(refElement.getBoundingClientRect().width);
        }

        return () => {
            if (observerRef.current && refElement) {
                observerRef.current.unobserve(refElement);
            }
        };
    }, []);

    const refreshWidth = () => {
        if (ref.current) {
            setWidth(ref.current.getBoundingClientRect().width);
        }
    };

    return { width, ref, refreshWidth };
};

3. 侧边栏切换时主动同步宽度状态

在Dashboard组件中监听侧边栏状态变化,主动调用refreshWidth确保状态与DOM宽度同步:

import './Dashboard.css'
import MonitorComponent from '../../components/Monitor/Monitor';
import DetailComponent from '../../components/Monitor/Detail';
import { Monitor } from '../../monitor'
import { useEffect, useMemo, useState } from 'react';
import { useWidth } from '../../hooks/useWidth';

interface DashboardProps {
  monitors: Monitor[]
  focused: Monitor | null,
  drafting: boolean,
  sidebarOpen?: boolean;
}

export default function Dashboard(props: DashboardProps) {
  const {
    monitors,
    focused,
    drafting,
    sidebarOpen,
  } = props;
  const isEditing = useMemo(() => focused || drafting, [focused, drafting]);
  const { width: dashboardWidth, ref: dashboardRef, refreshWidth } = useWidth<HTMLDivElement>();

  const [sorted, setSorted] = useState<Monitor[]>([])

  useEffect(() => {
    const sorted = monitors.sort((a, b) => {
      if (a.strategy.name > b.strategy.name) {
        return 1;
      } else {
        return -1;
      }
    });
    setSorted(sorted);
  }, [monitors])

  useEffect(() => {
    refreshWidth();
  }, [sidebarOpen, refreshWidth])

  const dashboardClasses = ['dashboard', isEditing && dashboardWidth < 700 ? 'single' : '']

  return (
    <div className={dashboardClasses.join(' ')} ref={dashboardRef}>
      <div className="dashboard_monitors">
        {sorted.map((n, i) =>
          <MonitorComponent key={i} monitor={n} />)
        }
      </div>

      {isEditing ?
        <div className="dashboard_activity">
          <DetailComponent monitor={focused} />
        </div>
        : null}
    </div>
  )
}

效果说明

  • CSS过渡让布局变化平滑,消除生硬跳动;
  • 主动刷新宽度确保侧边栏切换时状态与DOM同步,避免滞后渲染;
  • 保留布局空间的隐藏方式减少了重排次数,提升页面稳定性。

内容的提问来源于stack exchange,提问作者user12822512

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 16:17:21