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

如何在Preact中测量轮播卡片的X轴位移并判断可见性?

卡片移动时X轴数据不更新问题解决

我尝试测量卡片在父容器宽度范围内的移动情况,但点击按钮时X轴数据未更新。核心需求是当Client组件移出屏幕视野时执行特定操作,通过useRect钩子中的getBoundingClientRect方法实现,推测问题出在钩子依赖配置错误或ref传递失败。

相关代码片段

App.tsx

import { useState, useEffect } from 'preact/hooks'
import { createRef } from 'preact/compat';
import './app.css'
import Client from './components/Client';
import { useRect } from './hooks/useRect';

export function App() {
  const [translate, setTranslate] = useState<number>(0)

  function moveCard(prev: number) {
    setTranslate(prev += 20)
  }
  const contentRef = createRef<HTMLDivElement>();
  const carouselRect = useRect(contentRef)
  const {right: carouselEndPoint} = carouselRect;

  return (
    <>
    <button class='bg-slate-300 absolute top-2 left-2 ' onClick={() => {
      moveCard(translate)
    }}> Click to call function</button>
      <div ref={contentRef} className="grid grid-cols-4 bg-slate-200 gap-3 grid-flow-col w-4/5 mx-auto overflow-clip">
        <Client carouselEndPoint={carouselEndPoint} translate={`${translate}%`} setTranslate={setTranslate} name="1"/>
      </div>
    </>
  )
}

Client.tsx

import logo from '../assets/preact.svg'
import { useEffect } from 'preact/hooks'
import { useRect } from '../hooks/useRect'
import { StateUpdater, createRef } from 'preact/compat'

export interface ClientProps {
  name: string
  translate: string
  setTranslate: StateUpdater<number>
  carouselEndPoint: number
  width?: string | number
  // contentRef?: RefObject<HTMLDivElement>
}

function Client({name, translate, setTranslate, carouselEndPoint}: ClientProps) {

  const contentRef = createRef<HTMLDivElement>();
  const val = useRect(contentRef)
  const {left: clientX} = val;

  useEffect(() => {
    console.log(val);
    if(clientX >= carouselEndPoint) {
      console.log('hidden');
    }
  }, [translate])
  
  return (
      <div className="flex flex-col basis-auto bg-slate-300 items-center drop-shadow-md rounded-md w-full place-content-center transition-transform ease-linear duration-1000 overflow-clip" style={`transform: translateX(${translate})`} 
      ref={contentRef}>
        {/* client card */}
        <img className='h-full w-full' src={logo}/>
        <h3 className="text-5xl">{name}</h3>
      </div>
  )
}

export default Client;

useRect.tsx

import { RefObject } from 'preact'
import { useLayoutEffect, useCallback, useState } from "preact/hooks";

type RectResult = {
  bottom: number;
  height: number;
  left: number;
  right: number;
  top: number;
  width: number;
};

function getRect<T extends HTMLElement>(element?: T): RectResult {
  let rect: RectResult = {
    bottom: 0,
    height: 0,
    left: 0,
    right: 0,
    top: 0,
    width: 0,
  };
  if (element) rect = element.getBoundingClientRect();
  return rect;
}

export function useRect<T extends HTMLElement>(
  ref: RefObject<T>
): RectResult {
  const [rect, setRect] = useState<RectResult>(
    ref && ref.current ? getRect(ref.current) : getRect()
  );

  const handleResize = useCallback(() => {
    if (!ref.current) return;
    setRect(getRect(ref.current)); // Update client rect
  }, [ref]);

  useLayoutEffect(() => {
    const element = ref.current;
    if (!element) return;

    handleResize();

    if (typeof ResizeObserver === "function") {
      
      const resizeObserver = new ResizeObserver(() => handleResize());
      resizeObserver.observe(element);
      return () => {
        if (!resizeObserver) return;
        resizeObserver.disconnect();
        // resizeObserver = null;
      };
    } else {
      window.addEventListener("resize", handleResize); // Browser support, remove freely
      return () => window.removeEventListener("resize", handleResize);
    }
  }, [ref.current]);

  return rect;
}

问题分析与修复

核心问题

  1. ResizeObserver不监听位置变化:useRect钩子依赖ResizeObserver,但它仅监听元素尺寸变化,不会响应transform导致的位置移动,卡片平移时rect数据不会自动更新。
  2. useLayoutEffect依赖错误:useRect中useLayoutEffect的依赖是ref.current,但createRef的current属性变化不会触发组件重渲染,钩子无法感知元素位置变化。
  3. useEffect依赖不全:Client组件的useEffect仅监听translate,但clientX和carouselEndPoint变化时不会重新执行判断逻辑。
  4. 状态更新闭包陷阱:moveCard直接使用当前translate值更新状态,多次点击会导致状态更新不连续。

修复步骤

1. 修改useRect钩子,支持手动更新

让钩子返回rect数据和手动更新方法,用于主动触发位置数据刷新:

// useRect.tsx 修改后
export function useRect<T extends HTMLElement>(
  ref: RefObject<T>
): [RectResult, () => void] {
  const [rect, setRect] = useState<RectResult>(
    ref && ref.current ? getRect(ref.current) : getRect()
  );

  const updateRect = useCallback(() => {
    if (!ref.current) return;
    setRect(getRect(ref.current));
  }, [ref]);

  const handleResize = useCallback(() => {
    updateRect();
  }, [updateRect]);

  useLayoutEffect(() => {
    const element = ref.current;
    if (!element) return;

    updateRect();

    if (typeof ResizeObserver === "function") {
      const resizeObserver = new ResizeObserver(handleResize);
      resizeObserver.observe(element);
      return () => resizeObserver.disconnect();
    } else {
      window.addEventListener("resize", handleResize);
      return () => window.removeEventListener("resize", handleResize);
    }
  }, [ref, updateRect]);

  return [rect, updateRect];
}

2. 修复Client组件逻辑

在translate变化时手动更新位置数据,并完善useEffect依赖项:

// Client.tsx 修改后
function Client({name, translate, setTranslate, carouselEndPoint}: ClientProps) {

  const contentRef = createRef<HTMLDivElement>();
  const [val, updateRect] = useRect(contentRef)
  const {left: clientX} = val;

  useEffect(() => {
    // 平移后手动刷新位置数据
    updateRect();
    console.log(val);
    if(clientX >= carouselEndPoint) {
      console.log('hidden');
    }
  }, [translate, clientX, carouselEndPoint, updateRect])
  
  return (
      <div className="flex flex-col basis-auto bg-slate-300 items-center drop-shadow-md rounded-md w-full place-content-center transition-transform ease-linear duration-1000 overflow-clip" style={`transform: translateX(${translate})`} 
      ref={contentRef}>
        <img className='h-full w-full' src={logo}/>
        <h3 className="text-5xl">{name}</h3>
      </div>
  )
}

3. 修复App组件状态更新

使用函数式更新避免闭包陷阱:

// App.tsx 修改后
function moveCard() {
  setTranslate(prev => prev + 20)
}

// 按钮点击事件简化
<button class='bg-slate-300 absolute top-2 left-2 ' onClick={moveCard}> Click to call function</button>

额外优化

若需判断卡片是否移出父容器而非屏幕,可调整判断逻辑:当卡片的right小于父容器的left,或卡片的left大于父容器的right时,判定为移出父容器视野。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 22:35:01