Skip to content

fix(table): typewriter full-text flash on table workflow column#4694

Merged
TheodoreSpeaks merged 1 commit into
stagingfrom
fix/table-typewriter-discarded-render
May 21, 2026
Merged

fix(table): typewriter full-text flash on table workflow column#4694
TheodoreSpeaks merged 1 commit into
stagingfrom
fix/table-typewriter-discarded-render

Conversation

@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator

@TheodoreSpeaks TheodoreSpeaks commented May 21, 2026

Problem

The typewriter intermittently flashed the full value for one frame before typing it out. Reproducible during a large Run-all (verified in-browser: 60+ cells flashing per run).

Root cause

The reveal held a lagging nullable revealed state, and the caller renders revealed ?? kind.text. Under React 18 concurrent rendering, a committed render could observe revealed === null while text was the full value (the render-phase reset to '' didn't reliably win the race), so the ?? kind.text fallback painted the entire string for a frame before the type-on. Captured directly with a render-level probe: a committed render with text.length = 74, revealed = null.

Fix

Stop holding the revealed text in nullable state with a full-text fallback. Derive the slice from text + elapsed time during render:

  • For a non-null value the result is never null and never the full string on the frame text changes (elapsed ≈ 0 → 0 chars), so the ?? kind.text fallback can't fire → the flash is structurally impossible.
  • prevText is tracked in state (not a ref) so a render React starts then discards rolls it back and the change is re-detected on the committed render.
  • Same hook shape, same rAF cadence, same elapsed-time reveal (dropped frames catch up).

Verification

Instrumented the live build with a DOM MutationObserver over the grid:

  • Before: 60/60 animated cells showed 0 → <full> → 0 → 1 → 2 … (flash).
  • After: 0 flashes across 213 animated cells — all clean monotonic 0 → 1 → 2 … grows.

tsc + lint clean.

Type of Change

  • Bug fix

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel
Copy link
Copy Markdown

vercel Bot commented May 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped May 21, 2026 9:27am

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented May 21, 2026

PR Summary

Medium Risk
Medium risk because it changes render-time state updates and rAF-driven animation behavior in a hot UI path, which could affect cell rendering timing/perf if incorrect.

Overview
Prevents the table cell typewriter animation from briefly painting the full updated value by reworking useTypewriter to track the previous text in state (transactional under concurrent rendering) and derive the revealed substring from elapsed time during render.

The animation now uses a start-time ref plus a lightweight state “frame” tick to trigger re-renders, and adds guards for null/empty values so mount/scroll-in renders remain static while only subsequent updates animate.

Reviewed by Cursor Bugbot for commit e4f02aa. Bugbot is set up for automated code reviews on this repo. Configure here.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 21, 2026

Greptile Summary

This PR fixes an intermittent full-value flash in the useTypewriter hook that occurred under React 18 concurrent rendering. The root cause was a useRef being used to track the previous text value: a discarded render would mutate the ref (prevTextRef.current = text) but roll back the setRevealed('') state update, causing the committed render to skip the reset and flash revealed ?? kind.text for one frame.

  • Core fix: replaces prevTextRef (a useRef) with prevText (a useState), matching React's canonical "adjust state when a prop changes" pattern — state rolls back with discarded renders, guaranteeing the reset fires on the committed render.
  • Defensive guard: adds text === null || text.length === 0 to the animation effect to protect against a stale animateRef from a discarded render, and removes the now-unnecessary as string cast since TypeScript narrows text correctly after the guard.

Confidence Score: 5/5

Safe to merge; the change is a targeted, well-reasoned fix to a single hook with no external side-effects.

The switch from useRef to useState for prevText correctly implements the documented React "adjust state when a prop changes" pattern and closes the concurrent-rendering gap described in the PR. animateRef.current is still mutated during render, but the committed render always re-evaluates the condition (because state rolled back), so the value is re-applied deterministically. The defensive text === null || text.length === 0 guard in the animation effect is sound. No logic regressions are apparent.

No files require special attention.

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx Replaces ref-based previous-text tracking with state in useTypewriter, correctly fixing the concurrent-rendering flash; adds a null/empty guard in the animation effect.

Sequence Diagram

sequenceDiagram
    participant React as React (Concurrent)
    participant Render as Render Phase
    participant State as State (prevText, revealed)
    participant Ref as animateRef (ref)
    participant Effect as useEffect [text]

    note over React: text prop changes A → B

    React->>Render: Start render (may be discarded)
    Render->>State: "prevText !== text → setPrevText(B), setRevealed('')"
    Render->>Ref: "animateRef.current = true"
    React-->>Render: Discard render (concurrent preempt)
    note over State: State rolls back (prevText=A, revealed=null)
    note over Ref: Ref keeps mutation: animateRef=true

    React->>Render: Committed render
    Render->>State: "prevText (A) !== text (B) → setPrevText(B), setRevealed('')"
    Render->>Ref: "animateRef.current = true (re-set)"
    React->>Render: "Re-render (state settled: prevText=B)"
    Render-->>React: "prevText===text, no-op"
    React-->>React: "Commit with revealed=''"

    React->>Effect: Fire (text dependency changed)
    Effect->>Ref: "animateRef.current? true → animateRef=false"
    Effect-->>React: Start rAF typewriter animation
Loading

Reviews (1): Last reviewed commit: "fix(table): typewriter prevText in state..." | Re-trigger Greptile

…lash)

The reveal used a lagging nullable `revealed` state with a `revealed ?? kind.text`
fallback in the caller. Under React 18 concurrent rendering a committed render
could observe `revealed === null` while `text` was the full value, so the
fallback painted the entire string for one frame before the type-on — an
intermittent flash, reproducible on a large Run-all (verified in-browser: 60+
cells flashing).

Derive the revealed slice from `text` + elapsed time during render instead of
holding it in state. For a non-null value the result is never `null` and never
the full string on the frame `text` changes (elapsed ≈ 0 → 0 chars), so the
fallback can't fire. `prevText` is tracked in state (not a ref) so a discarded
render rolls it back and the change is re-detected on the committed render.
Verified via DOM MutationObserver: 0 flashes across 213 animated cells.
@TheodoreSpeaks TheodoreSpeaks force-pushed the fix/table-typewriter-discarded-render branch from 74171ca to e4f02aa Compare May 21, 2026 09:27
@TheodoreSpeaks TheodoreSpeaks changed the title fix(table): typewriter full-text flash under concurrent rendering fix(table): typewriter full-text flash (derive slice from elapsed time) May 21, 2026
@TheodoreSpeaks TheodoreSpeaks changed the title fix(table): typewriter full-text flash (derive slice from elapsed time) fix(table): typewriter full-text flash on table workflow column May 21, 2026
@TheodoreSpeaks TheodoreSpeaks merged commit 896eee3 into staging May 21, 2026
14 checks passed
@TheodoreSpeaks TheodoreSpeaks deleted the fix/table-typewriter-discarded-render branch May 21, 2026 09:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant