Skip to content

feat: add the streaming prop and streamingConfig.animation - #1150

Closed
hryhoriiK97 wants to merge 10 commits into
perf/ios-viewportfrom
feat/streaming-api
Closed

hryhoriiK97 wants to merge 10 commits into
perf/ios-viewportfrom
feat/streaming-api

Conversation

@hryhoriiK97

@hryhoriiK97 hryhoriiK97 commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

What/Why?

streamingAnimation sounds like a fade, but it switches on everything streaming does:

  • the streaming session: held-back tables, code blocks and media, and repaired inline syntax;
  • the pending code block header;
  • every arrival animation.

There was no way to stream without the fade, and the name hid what the prop is for.

This is the fifth PR of the streaming stack, stacked on #1149. It renames the prop to streaming and adds streamingConfig.animation.

The streaming stack

  1. feat: rebuild markdown streaming and add a streaming API #1140: measure and schedule.
  2. feat(core): prepare streamed markdown in one core session #1141: the core streaming session.
  3. perf: render only the tail of a streamed text segment #1146: render only the tail.
  4. perf(ios): draw only the part of a long document near the screen #1149: iOS memory, drawing only the part of a long document near the screen.
  5. This PR: streaming and streamingConfig.animation.
  6. The stream object: createMarkdownStream() with append / end / reset, text fed to a C++ buffer without a React render per token.
  7. Custom streaming animations: the 'gradient' preset and the animation object.

How

streaming replaces streamingAnimation in the types, the codegen specs and both platforms. streamingAnimation stays as a deprecated alias: streaming ?? streamingAnimation ?? false, so streaming wins when both are set. There's no runtime warning: react-native-streamdown sets streamingAnimation on every view, so a warning would reach all its users. The @deprecated tag marks it in editors.

streamingConfig.animation: 'fade' (default) or 'none'.

  • 'none' turns off the arrival animations:
    • the text tail fade;
    • block and table row fades;
    • the grow-in of blocks drawn by renderers.
  • Streaming itself keeps working: the held-back blocks, the repaired syntax and the pending code block header follow streaming as before.
  • Switching to 'none' mid-stream, or turning streaming off, shows what is still fading at once on both platforms. On Android, the segmented host now keeps one text fade animator per text view, as iOS does, so it can stop them.
  • animation changes no layout, so it isn't part of the measurement keys (iOS ENRMStreamingRenderEqual, Android stream-update hash).

Ending a stream: turning streaming off gives one unfiltered render, as before. The docs now say to do it.

onStreamingEnd: called when the view stops streaming, once streaming turns false. It fires once per stream, from where each host ends streaming, and not when the view unmounts mid-stream. iOS and Android only, like streaming. Later in the stack it also fires when a stream ends (#1151), and with smooth pacing only once the last words have appeared (#1159).

Testing

Unit tests:

  • Jest: streaming and animation reach the native view, animation defaults to 'fade', streamingAnimation alone arrives as streaming, and streaming={false} overrides streamingAnimation. onStreamingEnd reaches the callback.
  • Results: Jest 1,814 of 1,814, iOS 95 of 95, Android 119 of 119; ktlint and typecheck clean.

Devices: Release build on an iPhone 15 Pro (iOS 26.6.1). The 80 KB github "forest" and 66 KB commonmark "chat" streams from #1146 were each streamed three ways. A scratch counter logged the text tail fades started and the frames with an opacity animation on screen.

Stream Prop Updates shown Text fades Frames animating Final height = static
github forest streamingAnimation 1,315 1,125 5,808 yes
github forest streaming 1,312 1,121 5,529 yes
github forest streaming, animation: 'none' 1,313 0 0 yes
commonmark chat streamingAnimation 1,100 1,099 0 yes
commonmark chat streaming 1,100 1,099 0 yes
commonmark chat streaming, animation: 'none' 1,100 0 0 yes

The old and new props behave the same. 'none' starts no fade, and every stream still shows every update and ends at the static height.

onStreamingEnd, added later: iOS and Android build at this PR. At the top of the stack, checked with Argent on the iOS simulator and the Pixel 9 emulator: it fires once per stream in both flavors, with smooth pacing on and off, and when smooth is turned off during the final reveal.

On a Pixel 9 emulator (Android 16, Release build), the github forest stream through the deprecated streamingAnimation (sent to native as streaming) ends at 75,660.58, the same height as mounting the document in one go.

Docs

  • The API reference, the streaming guide and docs-md describe streaming, the deprecated alias and animation.
  • The prop's example is renamed with it and now stops streaming once the text is complete.
  • The reference said streamingConfig only works with flavor="github"; it works in both flavors.
  • onStreamingEnd has its reference entry and a line in the guide's Reference list.
  • check-examples, check-llms and yarn build pass.

Changes after review

  • Deprecation. react-native-streamdown is deprecated: this package holds back incomplete blocks and repairs half-written syntax natively. streamingAnimation logs a one-time development warning pointing to streaming. While it is on it also holds back incomplete blocks and repairs syntax, so turn it off when the stream ends. Migration notes are in both streaming guides, BREAKING_CHANGES.md and the README.
  • iOS stops a block's fade (tables included) without relying on UIKit's animation key. Android hashes only the streaming modes in the stream-update key.
  • The Jest mock calls onStreamingEnd when streaming turns off.
  • Docs: the docs-md streaming guide matches the Docusaurus guide; JSDoc, web tables and examples are updated, and the example app uses streaming.

Checked on the whole stack after these changes: core suite and fuzzing, Jest (1,838), Android unit tests (134), iOS unit tests (118), ktlint; streams on an iPhone 15 Pro and a Galaxy A16 call onStreamingEnd once and end at the static height, with CPU level or lower; the 106 Maestro flows give the same results and screenshots as before.

PR Checklist

  • Code compiles and runs on iOS
  • Code compiles and runs on Android
  • Updated documentation/README if applicable
  • Ran example app to verify changes
  • E2E tests are passing (not run)
  • Required E2E tests have been added (if applicable): Jest tests

🤖 Generated with Claude Code

hryhoriiK97 and others added 10 commits November 11, 2026 20:43
`streamingAnimation` turned on everything streaming does, not only the
fade: the streaming session (held-back tables, code blocks and media,
repaired inline syntax), the pending code block header and every
arrival animation. It is now `streaming`, with `streamingAnimation` kept
as a deprecated alias (`streaming` wins when both are set).

`streamingConfig.animation` ('fade' by default, or 'none') turns the
arrival animations off without turning streaming off: the text tail
fade, block and table row fades, and the grow-in of blocks drawn by
renderers. Switching it to 'none' (or turning streaming off) shows what
is still fading at once, on both platforms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`streamingAnimation` is documented as `streaming`, with the deprecated
alias noted, and `streamingConfig` gains `animation`. Its example is
renamed with the prop and now stops streaming once the text is
complete. Also corrects the reference: `streamingConfig` works in both
flavors, not only `github`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Called when the view stops streaming, once `streaming` turns false. It
fires once per stream, from where each host ends streaming, and not
when the view unmounts mid-stream. iOS and Android only, like
`streaming`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MARKDOWN_STREAMING.md still said "Both modes" and showed an always-on
streaming prop. It now mirrors the guide: streaming set while text
arrives and turned off at the end, onStreamingEnd, the three block modes
with mediaMode, and streamingConfig.animation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dropping `animation` from a copied map allocated two maps per measure and
would hash any key added to streamingConfig later. Hashing the three modes,
as iOS ENRMStreamingRenderEqual compares them, keeps the key to what changes
layout. Tests on both platforms pin that flipping the animation keeps a
stream's update on the fast path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UIView names its alpha animation after the key path, which is not
documented; removing all of the segment's animations and setting its alpha
does the same without that. Ending a stream and turning the animation off in
one update now finishes the animations once. The arrival helper's comment
also names the slot grow-in it gates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The animation JSDoc gains the renderer grow-in and Reduce Motion, and
onStreamingEnd its once-per-stream contract, as in the API reference. The
web stripped-props tables list onStreamingEnd, and the streamingConfig
examples end streaming once the document has arrived.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ation

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
react-native-streamdown is deprecated in favor of this package's own
streaming, which holds back incomplete blocks and repairs half-written
syntax natively. streamingAnimation now does that too while it is on, so
a view that keeps it on renders a finished message the same way. A
one-time development warning points to streaming, and both streaming
guides and the README now explain the migration instead of
recommending react-native-streamdown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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