Skip to content

fix(streamdown): re-render memoized components when node offset changes - #538

Closed
julesyoungberg wants to merge 3 commits into
vercel:mainfrom
julesyoungberg:feature/rerender-on-offset-change
Closed

fix(streamdown): re-render memoized components when node offset changes#538
julesyoungberg wants to merge 3 commits into
vercel:mainfrom
julesyoungberg:feature/rerender-on-offset-change

Conversation

@julesyoungberg

Copy link
Copy Markdown

Description

Fixes stale DOM when markdown is updated in the middle of an existing block. Memoized leaf components (ol, ul, p, headings, etc.) use sameNodePosition to skip re-renders, but it only compared line and column. When content changed without updating line/column (only offset, e.g. injecting a link mid-line), React kept stale output until Streamdown remounted.

sameNodePosition is shared by all memoized markdown components via sameClassAndNode, so this fix applies broadly rather than to a single element type.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)

Changes Made

Testing

  • All existing tests pass
  • Added new tests for the changes
  • Manually tested the changes
pnpm exec vitest run __tests__/components-rerender.test.tsx

All 18 tests in that file pass.

Checklist

  • My code follows the project's code style
  • I have performed a self-review of my own code
  • My changes generate no new warnings or errors
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have created a changeset (pnpm changeset)

Additional Notes

No documentation or inline comment changes — this is an internal comparator fix with no API surface change. The new test documents the edge case.

@vercel

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

@julesyoungberg is attempting to deploy a commit to the Vercel Team on Vercel.

A member of the Team first needs to authorize it.

@dasheidt

Copy link
Copy Markdown
Contributor

This fixes the case where content grows, but there is a case it cannot reach: a replacement of the same length moves no offset either.

const { container, rerender } = render(<Streamdown>{"Revenue grew"}</Streamdown>);
rerender(<Streamdown>{"Booking grew"}</Streamdown>);   // same length, same offsets

container.textContent; // "Revenue grew" — with or without this PR

start.offset is 0 and end.offset is 12 before and after, so the comparator still reports "unchanged" and the paragraph keeps rendering the old text. Same for a heading, a list item or a <strong> run of unchanged length.

The reason offset does not close it is that any position — line, column or offset — describes where the text came from, not what the component renders. I opened #584, which replaces the position comparison with React's own shallow prop comparison minus node, so the same test that motivated this PR passes, plus the same-length case and #570.

Not trying to steamroll this one — if the maintainers prefer the narrower change, the offset fix here is strictly better than what is on main today.

@farnabaz

Copy link
Copy Markdown
Collaborator

Thanks for PR @julesyoungberg, I'm closing this in favor of #584.

@farnabaz farnabaz closed this Aug 20, 2026
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.

3 participants