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

React画布首次渲染闪烁及Next.js Hydration错误解决求助

解决React SVG拖拽组件的首次渲染闪烁与Next.js Hydration错误问题

一、解决首次渲染闪烁问题

闪烁的核心原因是useDrag钩子中存在一段硬编码的useLayoutEffect逻辑,它会在组件初始化时强制将位置重置为{x:100, y:200},直接覆盖了你传入的随机初始位置,导致首次渲染出现位置跳变(闪烁)。

修复方式:
直接移除该硬编码的useLayoutEffect,让组件使用传入的初始位置完成渲染。

二、解决Next.js Hydration错误(随机初始位置)

Hydration错误的根源是服务端渲染(SSR)时生成的Math.random()值与客户端不一致,导致服务端渲染的DOM和客户端 hydration 后的DOM结构不匹配。

修复方式:
在客户端组件中,通过useEffect延迟生成随机位置,确保随机数仅在客户端环境生成,避免服务端与客户端的内容差异。


完整修改代码

1. 修正hooks/useDrag.ts

移除硬编码的位置重置逻辑,确保初始位置正常生效:

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

interface Position {
  x: number;
  y: number;
}

export default function useDrag<T extends SVGGraphicsElement>(
  initialPosition: Position = { x: 0, y: 0 },
) {
  const [isGrabbing, setIsGrabbing] = useState<boolean>(false);
  // 直接使用传入的初始位置作为state初始值
  const [position, setPosition] = useState<Position>(initialPosition);
  const [offset, setOffset] = useState<Position>({ x: 0, y: 0 });

  const dragRef = useRef<T | null>(null);

  // 移除导致闪烁的硬编码位置重置逻辑

  // 监听pointerdown事件
  useEffect(() => {
    const handlePointerDown = (e: PointerEvent) => {
      setIsGrabbing(true);
      setOffset({
        x: e.clientX - position.x,
        y: e.clientY - position.y,
      });
    };

    const dragEle = dragRef.current;
    dragEle?.addEventListener("pointerdown", handlePointerDown);

    return () => {
      dragEle?.removeEventListener("pointerdown", handlePointerDown);
    };
  }, [position]);

  // 监听pointermove和pointerup事件
  useEffect(() => {
    if (isGrabbing) {
      const handlePointerMove = (e: PointerEvent) => {
        setPosition({
          x: e.clientX - offset.x,
          y: e.clientY - offset.y,
        });
      };

      const handlePointerUp = () => {
        setIsGrabbing(false);
      };

      document.addEventListener("pointermove", handlePointerMove);
      document.addEventListener("pointerup", handlePointerUp);

      return () => {
        document.removeEventListener("pointermove", handlePointerMove);
        document.removeEventListener("pointerup", handlePointerUp);
      };
    }
  }, [isGrabbing, offset]);

  return {
    isGrabbing,
    position,
    dragRef,
  };
}

2. 修正Canvas.tsx

在客户端环境中生成随机初始位置,避免Hydration不匹配:

"use client";

import { useRef, useEffect, useState } from "react";
import useDrag from "@/hooks/useDrag";

export default function Canvas() {
  const containerRef = useRef<SVGSVGElement | null>(null);
  const [initialPos, setInitialPos] = useState({x:0, y:0});

  // 仅在客户端初始化时生成随机位置,规避SSR Hydration差异
  useEffect(() => {
    setInitialPos({
      x: Math.random() * 700, // 减去rect宽度100,避免超出画布
      y: Math.random() * 700, // 减去rect高度100,避免超出画布
    });
  }, []);

  const { isGrabbing, position, dragRef } = useDrag<SVGRectElement>(initialPos);

  return (
    <div className="flex min-h-screen items-center justify-center">
      <svg
        ref={containerRef}
        viewBox="0 0 800 800"
        className="relative h-[800px] w-[800px] bg-white"
      >
        <rect
          ref={dragRef}
          x={position.x}
          y={position.y}
          width={100}
          height={100}
          fill="red"
          className={isGrabbing ? "cursor-grabbing" : "cursor-grab"}
        />
      </svg>
    </div>
  );
}

关键说明

  • 移除useDrag中的硬编码位置重置逻辑,彻底消除首次渲染的位置跳变(闪烁)。
  • 通过useEffect在客户端生成随机位置,确保服务端渲染时初始位置为固定值,客户端 hydration 后再更新为随机值,完全规避Hydration不匹配错误。
  • 将随机数范围调整为Math.random() * 700,保证rect元素不会超出800x800的画布边界(rect宽高均为100)。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 01:20:54