Skip to content

Commit 74171ca

Browse files
fix(table): typewriter prevText in state, not ref (concurrent-render flash)
The render-phase reset used a ref (prevTextRef) for change detection. Under React 18 concurrent rendering, a render can be started then discarded; the ref mutation survives but the setRevealed('') rolls back. The committed render then sees prevTextRef === text, skips the reset, and `revealed ?? kind.text` paints the full value for one frame before the animation overwrites it — an intermittent full-text flash during runs with many streaming cells. Track the previous text in state so it rolls back with the discarded render and the reset re-fires on the committed one. Also guard the animation effect against a null/empty text (stale animateRef from a discarded render).
1 parent d730015 commit 74171ca

1 file changed

Lines changed: 11 additions & 5 deletions

File tree

  • apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -303,15 +303,21 @@ const TYPEWRITER_MS_PER_CHAR = 15
303303
*/
304304
function useTypewriter(text: string | null): string | null {
305305
const [revealed, setRevealed] = useState<string | null>(text)
306-
const prevTextRef = useRef<string | null>(text)
306+
// Track the previous text in STATE, not a ref. The reset below runs during
307+
// render; under concurrent rendering React can start a render, mutate a ref,
308+
// then discard that render. A ref keeps the mutation, so the committed render
309+
// sees prevText === text, skips the reset, and `revealed ?? kind.text` flashes
310+
// the full value for a frame. State rolls back with the discarded render, so
311+
// the reset re-fires on the committed one.
312+
const [prevText, setPrevText] = useState<string | null>(text)
307313
const mountedRef = useRef(false)
308314
const animateRef = useRef(false)
309315

310316
// Reset synchronously during render when `text` changes (not on first mount)
311317
// so no frame ever shows the full new value before the animation begins —
312318
// an effect-based reset lands one frame late and flashes the whole text.
313-
if (prevTextRef.current !== text) {
314-
prevTextRef.current = text
319+
if (prevText !== text) {
320+
setPrevText(text)
315321
const animate = mountedRef.current && text !== null && text.length > 0
316322
animateRef.current = animate
317323
setRevealed(animate ? '' : text)
@@ -322,9 +328,9 @@ function useTypewriter(text: string | null): string | null {
322328
}, [])
323329

324330
useEffect(() => {
325-
if (!animateRef.current) return
331+
if (!animateRef.current || text === null || text.length === 0) return
326332
animateRef.current = false
327-
const full = text as string
333+
const full = text
328334
const start = performance.now()
329335
let raf = 0
330336
const tick = (now: number) => {

0 commit comments

Comments
 (0)