Skip to content

Commit 958cdf8

Browse files
committed
fix: correct timing of prevContentLength for animate plugin
The previous implementation called resetPrevContentLength() in Block's function body, but Markdown (which calls processor.runSync synchronously) renders as a child component — AFTER Block's function body returns. This meant the animate plugin always saw prevContentLength=0 on every render. Fix: - Remove manual resetPrevContentLength() from Block's render body - Add self-reset inside rehypeAnimate after each run, so sibling blocks start clean (depth-first rendering ensures Markdown1 runs before Block2) - Read getLastRenderCharCount() at the TOP of Block's render body: since React renders depth-first, this value is from the PREVIOUS Markdown run (exactly the prevContentLength needed for the current render) - Remove stale useLayoutEffect approach (not needed with depth-first timing)
1 parent ef64268 commit 958cdf8

File tree

2 files changed

+14
-20
lines changed

2 files changed

+14
-20
lines changed

packages/streamdown/index.tsx

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -227,14 +227,16 @@ export const Block = memo(
227227
animatePlugin: animatePluginProp,
228228
...props
229229
}: BlockProps) => {
230-
// Track previous content length to prevent re-animation of already-visible content.
231-
// When a block's content grows during streaming, only new characters get animated.
230+
// Track the HAST character count from the PREVIOUS render pass.
231+
// React renders depth-first: this Block's function body runs, returns JSX, then
232+
// the child Markdown component runs (processor.runSync synchronously processes
233+
// content through rehype). On the current render, getLastRenderCharCount() still
234+
// holds the value from the PREVIOUS Markdown run — exactly what we need as
235+
// prevContentLength for this render. After Markdown runs, the plugin stores the
236+
// new count and self-resets prevContentLength so sibling blocks start clean.
232237
const prevContentLengthRef = useRef(0);
233-
234-
// Set prevContentLength on the animate plugin before the synchronous rehype render.
235-
// This is safe because React renders synchronously — the rehype pipeline will read
236-
// this value during the same synchronous render pass.
237238
if (animatePluginProp) {
239+
prevContentLengthRef.current = animatePluginProp.getLastRenderCharCount();
238240
animatePluginProp.setPrevContentLength(prevContentLengthRef.current);
239241
}
240242

@@ -245,24 +247,11 @@ export const Block = memo(
245247
? normalizeHtmlIndentation(content)
246248
: content;
247249

248-
const result = (
250+
return (
249251
<BlockIncompleteContext.Provider value={isIncomplete}>
250252
<Markdown {...props}>{normalizedContent}</Markdown>
251253
</BlockIncompleteContext.Provider>
252254
);
253-
254-
// Update prev content length after this render using the HAST character count
255-
// (not raw markdown length) to match the units used by charCounter in the animate plugin.
256-
prevContentLengthRef.current = animatePluginProp
257-
? animatePluginProp.getLastRenderCharCount()
258-
: 0;
259-
260-
// Reset so other blocks don't inherit this block's prevContentLength
261-
if (animatePluginProp) {
262-
animatePluginProp.resetPrevContentLength();
263-
}
264-
265-
return result;
266255
},
267256
(prevProps, nextProps) => {
268257
// Deep comparison for better memoization

packages/streamdown/lib/animate.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,11 @@ export function createAnimatePlugin(options?: AnimateOptions): AnimatePlugin {
189189
processTextNode(node, ancestors, config, charCounter)
190190
);
191191
config.lastRenderCharCount = charCounter.count;
192+
// Self-reset after each run so sibling blocks don't inherit this block's
193+
// prevContentLength. With React's depth-first rendering, this executes after
194+
// the current block's Markdown renders but before the next sibling block's
195+
// Markdown renders — so each block gets exactly its own prevContentLength.
196+
config.prevContentLength = 0;
192197
};
193198

194199
return {

0 commit comments

Comments
 (0)