fix(chat): use useLayoutEffect for scroll restore + auto-bottom to avoid mount flicker
The scroll-position memory introduced in 0.17.2 still produced a visible "chat appears at the top then jumps" frame when switching back into a conversation. Cause: both scroll-affecting effects (auto-bottom on new messages, restore on chat re-entry) used useEffect, which fires AFTER the browser paints the freshly-committed DOM. So users saw scrollTop=0 for one frame before the effect ran and corrected it. Switching both to useLayoutEffect moves the scroll write into the same commit phase as the message-list DOM update, so the very first paint already shows the correct position — single paint, no flicker. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { parseMessagePayload } from '@chat-app/shared/chat';
|
import { parseMessagePayload } from '@chat-app/shared/chat';
|
||||||
import { extractErrorCode } from '@chat-app/shared/i18n';
|
import { extractErrorCode } from '@chat-app/shared/i18n';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
@@ -406,7 +406,14 @@ export function ConversationPage() {
|
|||||||
if (id && messages.length > 0) markRead(id);
|
if (id && messages.length > 0) markRead(id);
|
||||||
}, [id, messages.length, markRead]);
|
}, [id, messages.length, markRead]);
|
||||||
|
|
||||||
useEffect(() => {
|
// useLayoutEffect: run synchronously after DOM commit, before the
|
||||||
|
// browser paints. Using useEffect here let one frame of "scrollTop = 0
|
||||||
|
// (top of list)" paint between message-list mount and the auto-scroll,
|
||||||
|
// which is exactly the "flickers to a different position, then jumps"
|
||||||
|
// glitch users saw when re-entering a chat. Layout-effect fires while
|
||||||
|
// the message list is in the DOM but before paint, so the first frame
|
||||||
|
// already shows the correct scroll position.
|
||||||
|
useLayoutEffect(() => {
|
||||||
const el = scrollRef.current;
|
const el = scrollRef.current;
|
||||||
if (!el || !stickToBottom) return;
|
if (!el || !stickToBottom) return;
|
||||||
el.scrollTop = el.scrollHeight;
|
el.scrollTop = el.scrollHeight;
|
||||||
@@ -423,7 +430,13 @@ export function ConversationPage() {
|
|||||||
const restoredForRef = useRef<string | null>(null);
|
const restoredForRef = useRef<string | null>(null);
|
||||||
const isRestoringRef = useRef(false);
|
const isRestoringRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
// useLayoutEffect, same reason as above: writing scrollTop here happens
|
||||||
|
// before the first paint of the freshly-mounted chat, so the user
|
||||||
|
// doesn't see a frame at scrollTop=0 before the jump to the saved
|
||||||
|
// position. Combined with the messages.length gate this means the
|
||||||
|
// re-entry shows the message list AT the saved scroll location in one
|
||||||
|
// single paint — no "loaded then jumped" effect.
|
||||||
|
useLayoutEffect(() => {
|
||||||
const el = scrollRef.current;
|
const el = scrollRef.current;
|
||||||
if (!el || !id) return;
|
if (!el || !id) return;
|
||||||
if (restoredForRef.current === id) return;
|
if (restoredForRef.current === id) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user