From c89b18ee955e47426952b2c99b2c344a9f2fc8b2 Mon Sep 17 00:00:00 2001 From: marshallern Date: Tue, 17 Mar 2026 16:37:50 +0300 Subject: [PATCH 1/3] fix: fix blocks flashing on mount at zero-zero position --- src/react-components/Block.tsx | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/react-components/Block.tsx b/src/react-components/Block.tsx index b3a5b5ba..7c533d98 100644 --- a/src/react-components/Block.tsx +++ b/src/react-components/Block.tsx @@ -1,4 +1,4 @@ -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"; @@ -119,6 +119,32 @@ 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; + } + const lastState = lastStateRef.current; + container.style.setProperty("--graph-block-geometry-x", `${geometry.x}px`); + container.style.setProperty("--graph-block-geometry-y", `${geometry.y}px`); + container.style.setProperty("--graph-block-geometry-width", `${geometry.width}px`); + container.style.setProperty("--graph-block-geometry-height", `${geometry.height}px`); + lastState.x = geometry.x; + lastState.y = geometry.y; + lastState.width = geometry.width; + lastState.height = geometry.height; + const { zIndex, order } = viewState.$viewState.value; + const newZIndex = (zIndex || 0) + (order || 0); + container.style.zIndex = `${newZIndex}`; + lastState.zIndex = newZIndex; + }, [state, viewState]); + /** * Update the block geometry and z-index when the state changes. */ From 6b37d2976dbd0369138badf98dbb19e225b4b8fa Mon Sep 17 00:00:00 2001 From: marshallern Date: Mon, 23 Mar 2026 17:33:12 +0300 Subject: [PATCH 2/3] fix: move the common part to the separate function --- src/react-components/Block.tsx | 43 ++++--------------- .../utils/applyBlockContainerLayout.ts | 43 +++++++++++++++++++ 2 files changed, 51 insertions(+), 35 deletions(-) create mode 100644 src/react-components/utils/applyBlockContainerLayout.ts diff --git a/src/react-components/Block.tsx b/src/react-components/Block.tsx index 7c533d98..e292d3da 100644 --- a/src/react-components/Block.tsx +++ b/src/react-components/Block.tsx @@ -6,6 +6,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"; @@ -130,19 +131,7 @@ function GraphBlockInner( if (!container || !geometry || !viewState) { return; } - const lastState = lastStateRef.current; - container.style.setProperty("--graph-block-geometry-x", `${geometry.x}px`); - container.style.setProperty("--graph-block-geometry-y", `${geometry.y}px`); - container.style.setProperty("--graph-block-geometry-width", `${geometry.width}px`); - container.style.setProperty("--graph-block-geometry-height", `${geometry.height}px`); - lastState.x = geometry.x; - lastState.y = geometry.y; - lastState.width = geometry.width; - lastState.height = geometry.height; - const { zIndex, order } = viewState.$viewState.value; - const newZIndex = (zIndex || 0) + (order || 0); - container.style.zIndex = `${newZIndex}`; - lastState.zIndex = newZIndex; + applyBlockContainerLayout(container, geometry, viewState, lastStateRef.current); }, [state, viewState]); /** @@ -151,39 +140,23 @@ function GraphBlockInner( 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..2dcde29d --- /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 }; +} From 003b420e3a356ec8b10e6735eecc29b6c2fbb65a Mon Sep 17 00:00:00 2001 From: marshallern Date: Mon, 23 Mar 2026 17:36:00 +0300 Subject: [PATCH 3/3] lint: lint fixes --- src/react-components/Block.tsx | 19 +++++++++++-------- .../utils/applyBlockContainerLayout.ts | 2 +- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/react-components/Block.tsx b/src/react-components/Block.tsx index e292d3da..d775fb53 100644 --- a/src/react-components/Block.tsx +++ b/src/react-components/Block.tsx @@ -1,4 +1,13 @@ -import React, { ForwardedRef, forwardRef, useEffect, useImperativeHandle, useLayoutEffect, 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"; @@ -120,7 +129,6 @@ 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. @@ -144,12 +152,7 @@ function GraphBlockInner( return; } - const { hasPositionChange } = applyBlockContainerLayout( - container, - geometry, - viewState, - lastStateRef.current, - ); + const { hasPositionChange } = applyBlockContainerLayout(container, geometry, viewState, lastStateRef.current); if (hasPositionChange) { if (!container.classList.contains("graph-block-container-moving")) { diff --git a/src/react-components/utils/applyBlockContainerLayout.ts b/src/react-components/utils/applyBlockContainerLayout.ts index 2dcde29d..b736bf40 100644 --- a/src/react-components/utils/applyBlockContainerLayout.ts +++ b/src/react-components/utils/applyBlockContainerLayout.ts @@ -12,7 +12,7 @@ export function applyBlockContainerLayout( container: HTMLDivElement, geometry: BlockLayoutGeometry, viewState: { $viewState: { value: { zIndex?: number; order?: number } } }, - lastState: BlockLayoutLastState, + 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;