diff --git a/src/react-components/Block.tsx b/src/react-components/Block.tsx index b3a5b5ba..d775fb53 100644 --- a/src/react-components/Block.tsx +++ b/src/react-components/Block.tsx @@ -1,4 +1,13 @@ -import React, { ForwardedRef, forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"; +import React, { + ForwardedRef, + forwardRef, + useEffect, + useImperativeHandle, + useLayoutEffect, + useMemo, + useRef, + useState, +} from "react"; import { TBlock } from "../components/canvas/blocks/Block"; import { Graph } from "../graph"; @@ -6,6 +15,7 @@ import { ESchedulerPriority } from "../lib/Scheduler"; import { useComputedSignal, useSchedulerDebounce, useSignalEffect } from "./hooks"; import { useBlockState } from "./hooks/useBlockState"; +import { applyBlockContainerLayout } from "./utils/applyBlockContainerLayout"; import { cn } from "./utils/cn"; import "./Block.css"; @@ -119,45 +129,37 @@ function GraphBlockInner( return () => viewState?.setHiddenBlock(false); }, [viewState, canvasVisible]); + /** + * Synchronously set initial block geometry before the browser paints + * to prevent blocks from flashing at position (0, 0) when mounting. + */ + useLayoutEffect(() => { + const geometry = state?.$geometry.value; + const container = containerRef.current; + if (!container || !geometry || !viewState) { + return; + } + applyBlockContainerLayout(container, geometry, viewState, lastStateRef.current); + }, [state, viewState]); + /** * Update the block geometry and z-index when the state changes. */ useSignalEffect(() => { const geometry = state?.$geometry.value; const container = containerRef.current; - const lastState = lastStateRef.current; if (!container || !geometry || !viewState) { return; } - const hasPositionChange = lastStateRef.current.x !== geometry.x || lastStateRef.current.y !== geometry.y; + const { hasPositionChange } = applyBlockContainerLayout(container, geometry, viewState, lastStateRef.current); if (hasPositionChange) { - container.style.setProperty("--graph-block-geometry-x", `${geometry.x}px`); - container.style.setProperty("--graph-block-geometry-y", `${geometry.y}px`); - lastState.x = geometry.x; - lastState.y = geometry.y; if (!container.classList.contains("graph-block-container-moving")) { container.classList.add("graph-block-container-moving"); } stopMoving(container); } - - const hasSizeChange = lastState.width !== geometry.width || lastState.height !== geometry.height; - if (hasSizeChange) { - container.style.setProperty("--graph-block-geometry-width", `${geometry.width}px`); - container.style.setProperty("--graph-block-geometry-height", `${geometry.height}px`); - lastState.width = geometry.width; - lastState.height = geometry.height; - } - - const { zIndex, order } = viewState.$viewState.value; - const newZIndex = (zIndex || 0) + (order || 0); - - if (lastState.zIndex !== newZIndex) { - container.style.zIndex = `${newZIndex}`; - lastState.zIndex = newZIndex; - } }, [containerRef, lastStateRef, state, viewState]); /** diff --git a/src/react-components/utils/applyBlockContainerLayout.ts b/src/react-components/utils/applyBlockContainerLayout.ts new file mode 100644 index 00000000..b736bf40 --- /dev/null +++ b/src/react-components/utils/applyBlockContainerLayout.ts @@ -0,0 +1,43 @@ +export type BlockLayoutGeometry = { x: number; y: number; width: number; height: number }; +export type BlockLayoutLastState = { x: number; y: number; width: number; height: number; zIndex: number }; + +/** + * Applies block geometry (position, size) and z-index to the container element, + * updating only the properties that have changed. Shared between mount-time + * (useLayoutEffect) and signal-driven (useSignalEffect) updates. + * + * @returns flags indicating which properties changed + */ +export function applyBlockContainerLayout( + container: HTMLDivElement, + geometry: BlockLayoutGeometry, + viewState: { $viewState: { value: { zIndex?: number; order?: number } } }, + lastState: BlockLayoutLastState +): { hasPositionChange: boolean; hasSizeChange: boolean } { + const hasPositionChange = lastState.x !== geometry.x || lastState.y !== geometry.y; + const hasSizeChange = lastState.width !== geometry.width || lastState.height !== geometry.height; + + if (hasPositionChange) { + container.style.setProperty("--graph-block-geometry-x", `${geometry.x}px`); + container.style.setProperty("--graph-block-geometry-y", `${geometry.y}px`); + lastState.x = geometry.x; + lastState.y = geometry.y; + } + + if (hasSizeChange) { + container.style.setProperty("--graph-block-geometry-width", `${geometry.width}px`); + container.style.setProperty("--graph-block-geometry-height", `${geometry.height}px`); + lastState.width = geometry.width; + lastState.height = geometry.height; + } + + const { zIndex, order } = viewState.$viewState.value; + const newZIndex = (zIndex || 0) + (order || 0); + + if (lastState.zIndex !== newZIndex) { + container.style.zIndex = `${newZIndex}`; + lastState.zIndex = newZIndex; + } + + return { hasPositionChange, hasSizeChange }; +}