From 2d05d280b2499568996c9e869fbfb6b7a1116a05 Mon Sep 17 00:00:00 2001 From: draedful Date: Tue, 2 Jun 2026 00:29:23 +0300 Subject: [PATCH] fix(anchor): eliminate position flash by switching to useLayoutEffect useEffect fires after paint, causing anchors to briefly appear at their default position before snapping to the correct coordinates. Replace with useSignalLayoutEffect (new synchronous variant) so the CSS variables are applied before the browser paints the frame. Also hide anchors whose position has not yet been calculated by defaulting the CSS fallback to -99999px instead of 0px. --- src/react-components/Anchor.css | 4 ++-- .../hooks/useBlockAnchorState.ts | 4 ++-- src/react-components/hooks/useSignal.ts | 20 ++++++++++++++++++- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/react-components/Anchor.css b/src/react-components/Anchor.css index 0f1a7b5b..c7d331ae 100644 --- a/src/react-components/Anchor.css +++ b/src/react-components/Anchor.css @@ -20,8 +20,8 @@ } .graph-block-anchor.graph-block-position-absolute { - --x: var(--graph-block-anchor-x, 0px); - --y: var(--graph-block-anchor-y, 0px); + --x: var(--graph-block-anchor-x, -99999px); + --y: var(--graph-block-anchor-y, -99999px); --x-offset: calc(var(--width) / 2); --y-offset: calc(var(--height) / 2); diff --git a/src/react-components/hooks/useBlockAnchorState.ts b/src/react-components/hooks/useBlockAnchorState.ts index 37f87db5..22cf31e9 100644 --- a/src/react-components/hooks/useBlockAnchorState.ts +++ b/src/react-components/hooks/useBlockAnchorState.ts @@ -3,7 +3,7 @@ import { Graph } from "../../graph"; import { AnchorState } from "../../store/anchor/Anchor"; import { useBlockState } from "./useBlockState"; -import { useComputedSignal, useSignalEffect } from "./useSignal"; +import { useComputedSignal, useSignalLayoutEffect } from "./useSignal"; export function useBlockAnchorState(graph: Graph, anchor: TAnchor): AnchorState | undefined { const blockState = useBlockState(graph, anchor.blockId); @@ -19,7 +19,7 @@ export function useBlockAnchorPosition( state: AnchorState | undefined, anchorContainerRef: React.MutableRefObject | undefined ) { - useSignalEffect(() => { + useSignalLayoutEffect(() => { if (!state || !anchorContainerRef?.current) { return; } diff --git a/src/react-components/hooks/useSignal.ts b/src/react-components/hooks/useSignal.ts index 6cb21c2f..20f02a2d 100644 --- a/src/react-components/hooks/useSignal.ts +++ b/src/react-components/hooks/useSignal.ts @@ -1,4 +1,4 @@ -import { DependencyList, useCallback, useEffect, useMemo, useSyncExternalStore } from "react"; +import { DependencyList, useCallback, useEffect, useLayoutEffect, useMemo, useSyncExternalStore } from "react"; import { computed, effect } from "@preact/signals-core"; import type { Signal } from "@preact/signals-core"; @@ -65,3 +65,21 @@ export function useSignalEffect(effectFn: () => void, deps: DependencyList) { return effect(() => handle()); }, deps); } + +/** + * Like useSignalEffect but runs synchronously after DOM mutations, before the browser paints. + * Use when signal changes must be reflected in the DOM without a visible frame delay. + * + * @example + * ```tsx + * useSignalLayoutEffect(() => { + * ref.current?.style.setProperty("--x", `${signal.value}px`); + * }, [signal]); + * ``` + */ +export function useSignalLayoutEffect(effectFn: () => void, deps: DependencyList) { + const handle = useFn(effectFn); + useLayoutEffect(() => { + return effect(() => handle()); + }, deps); +}