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

TypeScript中使用Intersection Observer与useRef出现类型错误求助

解决React+TypeScript中Intersection Observer的类型错误及动画触发问题

问题描述

我有一个运行正常的文本动画,希望添加Intersection Observer,让动画仅在滚动到Box组件时触发。通过React钩子useRef获取目标元素引用并绑定到Box(ref={containerRef}),实现了自定义钩子useElementOnScreen封装观察逻辑,但遇到两个TypeScript错误:

  1. 在containerRef?.current处报错:
Argument of type 'IntersectionObserver' is not assignable to parameter of type 'Element'.
  Type 'IntersectionObserver' is missing the following properties from type 'Element': attributes, classList, className, clientHeight, and 160 more.
  1. ref={containerRef}触发类型错误:
The expected type comes from property 'ref' which is declared here on type 'IntrinsicAttributes & { component: ElementType<any>; } & SystemProps<Theme> & { children?: ReactNode; component?: ElementType<...> | undefined; ref?: Ref<...> | undefined; sx?: SxProps<...> | undefined; } & CommonProps & Omit<...>'

动画说明:TopAnimateBlock和BottomAnimateBlock的numOfLine属性控制块内行数,BottomAnimateBlock的delayTopLine属性需与TopAnimateBlock的numOfLine值一致,以等待顶部动画完成。


错误原因分析

  1. useRef类型错误:给containerRef指定了错误的类型IntersectionObserver | null,它实际应指向DOM元素,正确类型为HTMLDivElement | null(MUI的Box默认渲染div)。
  2. 动画触发逻辑缺失:原代码未根据isVisible状态控制动画执行,页面加载时会直接触发动画,不符合“滚动到组件时才触发”的需求。

修改后的完整代码

import { Box, Typography } from '@mui/material';
import React, { useRef, useEffect, useState } from 'react';
import styled, { keyframes } from 'styled-components';

const showTopText = keyframes`
  0% { transform: translate3d(0, 100% , 0); }
  40%, 60% { transform: translate3d(0, 50%, 0); }
  100% { transform: translate3d(0, 0, 0); }
`;
const showBottomText = keyframes`
  0% { transform: translate3d(0, -100%, 0); }
  100% { transform: translate3d(0, 0, 0); }
`;

const Section = styled.section`
  width: calc(100% + 10vmin);
  display: flex;
  flex-flow: column;
  padding: 2vmin 0;
  overflow: hidden;
  &:last-child {
    border-top: 1vmin solid white;
  }
`;

const Block = styled.div<{ numOfLine: number }>`
  position: relative;
`;
// 添加isVisible props控制动画是否执行
const TopAnimateBlock = styled(Block)<{ numOfLine: number; isVisible: boolean }>`
  transform: translateY(calc(100% * ${props => props.numOfLine}));
  ${props => props.isVisible && `
    animation: ${showTopText} calc(0.5s * ${props.numOfLine}) forwards;
    animation-delay: 0.5s;
  `}
`;
const BottomAnimateBlock = styled(Block)<{ numOfLine: number; delayTopLine: number; isVisible: boolean }>`
  transform: translateY(calc(-100% * ${props => props.numOfLine}));
  ${props => props.isVisible && `
    animation: ${showBottomText} calc(0.5s * ${props.numOfLine}) forwards;
    animation-delay: calc(0.7s * ${props.delayTopLine});
  `}
`;

const TextStyle = styled.p<{ color: string }>`
  font-family: Roboto, Arial, sans-serif;
  font-size: 12vmin;
  color: ${props => props.color};
`;

// 修正自定义钩子的类型定义
const useElementOnScreen = (options: IntersectionObserverInit) => {
    // 指定ref类型为HTMLDivElement,匹配Box默认渲染的DOM元素
    const containerRef = useRef<HTMLDivElement | null>(null);
    const [isVisible, setIsVisible] = useState(false);

    const callbackFunction = (entries: IntersectionObserverEntry[]) => {
        const [entry] = entries;
        setIsVisible(entry.isIntersecting);
    };

    useEffect(() => {
        const observer = new IntersectionObserver(callbackFunction, options);

        if (containerRef.current) observer.observe(containerRef.current);

        return () => {
            if (containerRef.current) observer.unobserve(containerRef.current);
        };
    }, [options]); // 移除containerRef依赖,因为ref引用是稳定的

    return [containerRef, isVisible] as const;
};

export function Details() {
    const [containerRef, isVisible] = useElementOnScreen({
        root: null,
        rootMargin: '0px',
        threshold: 1.0,
    });

    return (
        <>
            <Typography>Scroll Down</Typography>
            {/* ref类型现在匹配,无类型错误 */}
            <Box ref={containerRef}>
                <Section>
                    <TopAnimateBlock numOfLine={2} isVisible={isVisible}>
                        <TextStyle color="grey">mimicking</TextStyle>
                        <TextStyle color="white">apple's design</TextStyle>
                    </TopAnimateBlock>
                </Section>
                <Section>
                    <BottomAnimateBlock numOfLine={1} delayTopLine={2} isVisible={isVisible}>
                        <TextStyle color="white">for the win!</TextStyle>
                    </BottomAnimateBlock>
                </Section>
            </Box>
        </>
    );
};

关键修改点说明

  • 修正useRef类型:将containerRef类型从IntersectionObserver | null改为HTMLDivElement | null,确保指向正确的DOM元素。
  • 条件触发动画:给动画组件添加isVisible属性,仅当元素进入视口时才应用动画样式,实现滚动触发效果。
  • 优化钩子依赖:移除useEffect中的containerRef依赖,因为useRef返回的引用在组件生命周期内保持稳定。
  • 明确类型定义:为useElementOnScreen的参数和回调函数添加精准的TypeScript类型,提升代码安全性。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 09:18:13