Repository navigation
fix: don't set state in useStreamdownMarkdown's effect (Maximum update depth exceeded while streaming) - #38
Open
petar-studocu wants to merge 2 commits into
Conversation
The effect called setIsStreaming(true) on every markdown change, so each chunk rendered twice. When the markdown comes from a synchronous store (e.g. useSyncExternalStore in AI SDK useChat) and the JS thread is busy, each of those follow-up updates is left pending when the chunk's sync commit finishes, React counts it as a nested update, and after ~50 chunks it throws "Maximum update depth exceeded" mid-stream. State is now only set from the worklet callback, and isStreaming is derived from which markdown the last processed result belongs to. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a worklet round-trip takes longer than the gap between markdown updates (a fast stream on a busy JS thread), the latest-only check drops every result and the text freezes until the stream pauses. Apply any result newer than the one shown instead; clearing the markdown marks older results as stale. isStreaming stays true until the latest markdown has been processed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
useStreamdownMarkdowncallssetIsStreaming(true)inside its effect on every markdown change. With a fast-streaming source this can make React throw "Maximum update depth exceeded" mid-stream. This PR only sets state from the worklet callback and derivesisStreamingfrom which markdown the last processed result belongs to.The bug
The usual way to stream into
StreamdownTextis AI SDKuseChat, which pushes each chunk throughuseSyncExternalStore. That makes every chunk a SyncLane render.setIsStreaming(true)is still pending at that check, so React counts the commit as a nested update.setStatethrows. That is usually the chat store's own update, so the stream fails.In our app (Studocu), this caused about 25% of Ace chat replies to fail on low-end Android after we adopted
StreamdownText.The fix
setResult({source, processed})is called only from the worklet callback.isStreamingis nowresult.source !== markdown: true until the latest markdown has been processed. The meaning is the same as before.{processedMarkdown: '', isStreaming: false}directly, and results from jobs started before the clear are dropped.remendConfigKeygating from fix: depend on remendConfig content instead of its identity #30 is unchanged.Tests
src/__tests__/useStreamdownMarkdown.test.tsx: mounting and each markdown change render exactly once until the worklet result arrives. It also checksprocessedMarkdownandisStreamingthrough a full cycle.yarn test,yarn typecheckandyarn lintpass.StreamdownTextinto a long thread threw at chunk 53 on 0.2.0 and passes with this change applied as a patch.🤖 Generated with Claude Code